mirror of
https://github.com/vuejs/vue-cli.git
synced 2026-03-13 20:51:41 -05:00
docs: improve documentation for plugin development
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
|
||||
> This is the branch for `@vue/cli` 3.0.
|
||||
|
||||
**Status: alpha**
|
||||
## Status: alpha
|
||||
|
||||
Certain combinations of plugins may not work properly, and things may break until we reach beta phase. Do not use in production yet unless you are adventurous.
|
||||
|
||||
## Install
|
||||
|
||||
|
||||
132
docs/Plugin.md
132
docs/Plugin.md
@@ -1,15 +1,14 @@
|
||||
# Plugin Development Guide
|
||||
|
||||
#### Important Development Note
|
||||
|
||||
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:
|
||||
|
||||
1. the package always exist in this repo's root `node_modules` so that we don't have to reinstall them on every test.
|
||||
|
||||
2. `yarn.lock` stays consistent so that CI can better use it for inferring caching behavior.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- [Creator](#creator)
|
||||
- [Service](#service)
|
||||
- [CLI Plugin](#cli-plugin)
|
||||
- [Service Plugin](#service-plugin)
|
||||
- [Generator](#generator)
|
||||
- [Prompts](#prompts)
|
||||
|
||||
There are two major parts of the system:
|
||||
|
||||
- `@vue/cli`: globally installed, exposes the `vue create <app>` command;
|
||||
@@ -25,17 +24,36 @@ Both utilize a plugin-based architecture.
|
||||
|
||||
[Service][service-class] 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.
|
||||
|
||||
### Plugin
|
||||
### CLI Plugin
|
||||
|
||||
Plugins are locally installed into the project as dependencies. `@vue/cli-service`'s [built-in commands][commands] and [config modules][config] are also all implemented as plugins. This repo also contains a number of plugins that are published as individual packages.
|
||||
A CLI plugin is an npm package that can add additional features to a `@vue/cli` project. It should always contain a [Service Plugin](#service-plugin) as its main export, and can optionally contain a [Generator](#generator) and a [Prompt File](#prompts-for-3rd-party-plugins).
|
||||
|
||||
A plugin should export a function which receives two arguments:
|
||||
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 Plugin
|
||||
|
||||
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][commands] and [config modules][config] are also all implemented as service plugins.
|
||||
|
||||
A service plugin should export a function which receives two arguments:
|
||||
|
||||
- A [PluginAPI][plugin-api] instance
|
||||
|
||||
- Project local options specified in `vue.config.js`, or in the `"vue-cli"` field in `package.json`.
|
||||
- An object containing project local options specified in `vue.config.js`, or in the `"vue-cli"` field in `package.json`.
|
||||
|
||||
The API allows plugins to extend/modify the internal webpack config for different environments and inject additional commands to `vue-cli-service`. Example:
|
||||
The API allows service plugins to extend/modify the internal webpack config for different environments and inject additional commands to `vue-cli-service`. Example:
|
||||
|
||||
``` js
|
||||
module.exports = (api, projectOptions) => {
|
||||
@@ -54,9 +72,53 @@ module.exports = (api, projectOptions) => {
|
||||
}
|
||||
```
|
||||
|
||||
#### Environment Variables in Service Plugins
|
||||
|
||||
An important thing to note about env variables is knowing when they are resolved. Typically, a command like `vue-cli-service serve` or `vue-cli-service build` will always call `api.setMode()` as the first thing it does. However, this also means those env variables may not yet be available when a service plugin is invoked:
|
||||
|
||||
``` js
|
||||
module.exports = api => {
|
||||
process.env.NODE_ENV // may not be resolved yet
|
||||
|
||||
api.regsiterCommand('build', () => {
|
||||
api.setMode('production')
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Instead, it's safer to rely on env variables in `configureWebpack` or `chainWebpack` functions, which are called lazily only when `api.resolveWebpackConfig()` is finally called:
|
||||
|
||||
``` js
|
||||
module.exports = api => {
|
||||
api.configureWebpack(config => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### Custom Options for 3rd Party Plugins
|
||||
|
||||
The exports from `vue.config.js` will be [validated against a schema](https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-service/lib/options.js#L3) 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`:
|
||||
|
||||
``` js
|
||||
module.exports = {
|
||||
pluginOptions: {
|
||||
foo: { /* ... */ }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The 3rd party plugin can read `projectOptions.pluginOptions.foo` to determine conditional configurations.
|
||||
|
||||
### Generator
|
||||
|
||||
A plugin published as a package can also contain a `generator.js` or `generator/index.js` file. The generator inside a plugin will be invoked after the plugin is installed.
|
||||
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][generator-api] allows a generator to inject additional dependencies or fields into `package.json` and add files to the project.
|
||||
|
||||
@@ -64,19 +126,25 @@ A generator should export a function which receives three arguments:
|
||||
|
||||
1. A `GeneratorAPI` instance;
|
||||
|
||||
2. The generator options for this plugin. These options are resolved during the prompt phase of project creation, or loaded from a saved `~/.vuerc`. For example, if the saved `~/.vuerc` looks like this:
|
||||
2. 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:
|
||||
|
||||
``` json
|
||||
{
|
||||
"plugins": {
|
||||
"@vue/cli-plugin-foo": { "option": "bar" }
|
||||
"presets" : {
|
||||
"foo": {
|
||||
"plugins": {
|
||||
"@vue/cli-plugin-foo": { "option": "bar" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then the plugin `@vue/cli-plugin-foo` will receive `{ option: 'bar' }` as its second argument.
|
||||
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.
|
||||
|
||||
3. The entire `.vuerc` object will be passed as the third 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](#prompts-for-3rd-party-plugins)).
|
||||
|
||||
3. The entire preset (`presets.foo`) will be passed as the third argument.
|
||||
|
||||
**Example:**
|
||||
|
||||
@@ -98,9 +166,11 @@ module.exports = (api, options, rootOptions) => {
|
||||
}
|
||||
```
|
||||
|
||||
### Prompt Modules
|
||||
### Prompts
|
||||
|
||||
Currently, only built-in plugins have the ability to customize the prompts when creating a new project, and the prompt modules are located [inside the `@vue/cli` package][prompt-modules].
|
||||
#### Prompts for Built-in Plugins
|
||||
|
||||
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][prompt-modules].
|
||||
|
||||
A prompt module should export a function that receives a [PromptModuleAPI][prompt-api] instance. The prompts are presented using [inquirer](https://github.com/SBoudrias/Inquirer.js) under the hood:
|
||||
|
||||
@@ -133,6 +203,26 @@ module.exports = api => {
|
||||
}
|
||||
```
|
||||
|
||||
#### Prompts for 3rd Party Plugins
|
||||
|
||||
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 `prompt.js` in its root directory, it will be used during invocation. The file should export an array of [Questions](https://github.com/SBoudrias/Inquirer.js#question) 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.:
|
||||
|
||||
``` sh
|
||||
vue invoke my-plugin --mode awesome
|
||||
```
|
||||
|
||||
## Note on Development of Core Plugins
|
||||
|
||||
> This section only applies if you are working on a built-in plugin inside this very repository.
|
||||
|
||||
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:
|
||||
|
||||
1. the package always exist in this repo's root `node_modules` so that we don't have to reinstall them on every test.
|
||||
|
||||
2. `yarn.lock` stays consistent so that CI can better use it for inferring caching behavior.
|
||||
|
||||
[creator-class]: https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli/lib/Creator.js
|
||||
[service-class]: https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-service/lib/Service.js
|
||||
[generator-api]: https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli/lib/GeneratorAPI.js
|
||||
|
||||
@@ -1 +1,52 @@
|
||||
# WIP
|
||||
|
||||
## Introduction
|
||||
|
||||
## The CLI
|
||||
|
||||
## Configuration
|
||||
|
||||
### Vue CLI options
|
||||
|
||||
### Modes and Environment Variables
|
||||
|
||||
### Webpack
|
||||
|
||||
- #### Basic Configuration
|
||||
|
||||
- #### Chaining
|
||||
|
||||
- #### Using Resolved Config as a File
|
||||
|
||||
### Babel
|
||||
|
||||
- link to: babel preset
|
||||
- link to: babel plugin
|
||||
|
||||
### CSS
|
||||
|
||||
- #### PostCSS
|
||||
|
||||
- #### CSS Modules
|
||||
|
||||
- #### Other Pre-Processors
|
||||
|
||||
### ESLint
|
||||
|
||||
- link to: eslint plugin
|
||||
|
||||
### TypeScript
|
||||
|
||||
- link to: typescript plugin
|
||||
|
||||
### Unit Testing
|
||||
|
||||
- #### Jest
|
||||
|
||||
- #### Mocha (via `mocha-webpack`)
|
||||
|
||||
### E2E Testing
|
||||
|
||||
- #### Cypress
|
||||
|
||||
- #### Nightwatch
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
const path = require('path')
|
||||
|
||||
module.exports = class PluginAPI {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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-)-cli-plugin- prefix
|
||||
* @return {boolean}
|
||||
*/
|
||||
hasPlugin (id) {
|
||||
const prefixRE = /^(@vue\/|vue-)cli-plugin-/
|
||||
return this.service.plugins.some(p => {
|
||||
@@ -17,8 +33,13 @@ module.exports = class PluginAPI {
|
||||
})
|
||||
}
|
||||
|
||||
// set project mode.
|
||||
// this should be called by any registered command as early as possible.
|
||||
/**
|
||||
* Set project mode and resolve env variables for that mode.
|
||||
* this should be called by any registered command as early as possible, and
|
||||
* should be called only once per command.
|
||||
*
|
||||
* @param {string} mode
|
||||
*/
|
||||
setMode (mode) {
|
||||
process.env.VUE_CLI_MODE = mode
|
||||
// by default, NODE_ENV and BABEL_ENV are set to "development" unless mode
|
||||
@@ -31,6 +52,19 @@ module.exports = class PluginAPI {
|
||||
this.service.loadEnv(mode)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -39,23 +73,63 @@ module.exports = class PluginAPI {
|
||||
this.service.commands[name] = { fn, opts }
|
||||
}
|
||||
|
||||
/**
|
||||
* Regsiter 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Regsiter
|
||||
* - 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`
|
||||
* instnace 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.
|
||||
* Typically, you should call `setMode` before calling this.
|
||||
*
|
||||
* @return {object} Raw webpack config.
|
||||
*/
|
||||
resolveWebpackConfig () {
|
||||
return this.service.resolveWebpackConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
|
||||
configureDevServer (fn) {
|
||||
this.service.devServerConfigFns.push(fn)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PluginAPI
|
||||
|
||||
@@ -22,7 +22,13 @@ function getLink (id) {
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = class GeneratorAPI {
|
||||
class GeneratorAPI {
|
||||
/**
|
||||
* @param {string} id - Id of the owner plugin
|
||||
* @param {Generator} generator - The invoking Generator instance
|
||||
* @param {object} options - generator options passed to this plugin
|
||||
* @param {object} rootOptions - root options (the entire preset)
|
||||
*/
|
||||
constructor (id, generator, options, rootOptions) {
|
||||
this.id = id
|
||||
this.generator = generator
|
||||
@@ -43,6 +49,11 @@ module.exports = class GeneratorAPI {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the data when rendering templates.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_resolveData (additionalData) {
|
||||
return Object.assign({
|
||||
options: this.options,
|
||||
@@ -51,10 +62,47 @@ module.exports = class GeneratorAPI {
|
||||
}, additionalData)
|
||||
}
|
||||
|
||||
injectFileMiddleware (middleware) {
|
||||
/**
|
||||
* Inject a file processing middleware.
|
||||
*
|
||||
* @private
|
||||
* @param {FileMiddleware} middleware - A middleware function that receives the
|
||||
* virtual files tree object, and an ejs render function. Can be async.
|
||||
*/
|
||||
_injectFileMiddleware (middleware) {
|
||||
this.generator.fileMiddlewares.push(middleware)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.generator.context, _path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the project has a given plugin.
|
||||
*
|
||||
* @param {string} id - Plugin id, can omit the (@vue/|vue-)-cli-plugin- prefix
|
||||
* @return {boolean}
|
||||
*/
|
||||
hasPlugin (id) {
|
||||
return this.generator.hasPlugin(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the package.json of the project.
|
||||
* Nested fields are deep-merged unless `{ merge: false }` is passed.
|
||||
* Also resolves dependency conflicts between plugins.
|
||||
* Tool configuration fields may be extracted into standalone files before
|
||||
* files are written to disk.
|
||||
*
|
||||
* @param {object} fields - Fields to merge.
|
||||
* @param {object} [options] - pass { merge: false } to disable deep merging.
|
||||
*/
|
||||
extendPackage (fields, options = { merge: true }) {
|
||||
const pkg = this.generator.pkg
|
||||
const toMerge = isFunction(fields) ? fields(pkg) : fields
|
||||
@@ -81,13 +129,24 @@ module.exports = class GeneratorAPI {
|
||||
}
|
||||
}
|
||||
|
||||
render (fileDir, additionalData = {}, ejsOptions = {}) {
|
||||
/**
|
||||
* Render template files into the virtual files tree object.
|
||||
*
|
||||
* @param {string | object | FileMiddleware} source -
|
||||
* Can be one of:
|
||||
* - relative path to a directory;
|
||||
* - Object hash of { sourceTemplate: targetFile } mappings;
|
||||
* - a custom file middleware function.
|
||||
* @param {object} [additionalData] - additional data available to templates.
|
||||
* @param {object} [ejsOptions] - options for ejs.
|
||||
*/
|
||||
render (source, additionalData = {}, ejsOptions = {}) {
|
||||
const baseDir = extractCallDir()
|
||||
if (isString(fileDir)) {
|
||||
fileDir = path.resolve(baseDir, fileDir)
|
||||
this.injectFileMiddleware(async (files) => {
|
||||
if (isString(source)) {
|
||||
source = path.resolve(baseDir, source)
|
||||
this._injectFileMiddleware(async (files) => {
|
||||
const data = this._resolveData(additionalData)
|
||||
const _files = await globby(['**/*'], { cwd: fileDir })
|
||||
const _files = await globby(['**/*'], { cwd: source })
|
||||
for (const rawPath of _files) {
|
||||
let filename = path.basename(rawPath)
|
||||
// dotfiles are ignored when published to npm, therefore in templates
|
||||
@@ -96,7 +155,7 @@ module.exports = class GeneratorAPI {
|
||||
filename = `.${filename.slice(1)}`
|
||||
}
|
||||
const targetPath = path.join(path.dirname(rawPath), filename)
|
||||
const sourcePath = path.resolve(fileDir, rawPath)
|
||||
const sourcePath = path.resolve(source, rawPath)
|
||||
const content = renderFile(sourcePath, data, ejsOptions)
|
||||
// only set file if it's not all whitespace, or is a Buffer (binary files)
|
||||
if (Buffer.isBuffer(content) || /[^\s]/.test(content)) {
|
||||
@@ -104,37 +163,40 @@ module.exports = class GeneratorAPI {
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (isObject(fileDir)) {
|
||||
this.injectFileMiddleware(files => {
|
||||
} else if (isObject(source)) {
|
||||
this._injectFileMiddleware(files => {
|
||||
const data = this._resolveData(additionalData)
|
||||
for (const targetPath in fileDir) {
|
||||
const sourcePath = path.resolve(baseDir, fileDir[targetPath])
|
||||
for (const targetPath in source) {
|
||||
const sourcePath = path.resolve(baseDir, source[targetPath])
|
||||
const content = renderFile(sourcePath, data, ejsOptions)
|
||||
if (Buffer.isBuffer(content) || content.trim()) {
|
||||
files[targetPath] = content
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (isFunction(fileDir)) {
|
||||
this.injectFileMiddleware(fileDir)
|
||||
} else if (isFunction(source)) {
|
||||
this._injectFileMiddleware(source)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a file middleware that will be applied after all normal file
|
||||
* middelwares have been applied.
|
||||
*
|
||||
* @param {FileMiddleware} cb
|
||||
*/
|
||||
postProcessFiles (cb) {
|
||||
this.generator.postProcessFilesCbs.push(cb)
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a callback to be called when the files have been written to disk.
|
||||
*
|
||||
* @param {function} cb
|
||||
*/
|
||||
onCreateComplete (cb) {
|
||||
this.generator.completeCbs.push(cb)
|
||||
}
|
||||
|
||||
resolve (_path) {
|
||||
return path.resolve(this.generator.context, _path)
|
||||
}
|
||||
|
||||
hasPlugin (id) {
|
||||
return this.generator.hasPlugin(id)
|
||||
}
|
||||
}
|
||||
|
||||
function extractCallDir () {
|
||||
@@ -152,3 +214,5 @@ function renderFile (name, data, ejsOptions) {
|
||||
}
|
||||
return ejs.render(fs.readFileSync(name, 'utf-8'), data, ejsOptions)
|
||||
}
|
||||
|
||||
module.exports = GeneratorAPI
|
||||
|
||||
Reference in New Issue
Block a user