How to build a Vue CLI plugin

n_tepluhina

Natalia Tepluhina

Posted on October 15, 2018

How to build a Vue CLI plugin

This article is cross-posted from ITNEXT

If you're using Vue framework, you probably already know what Vue CLI is. It's a full system for rapid Vue.js development, providing project scaffolding and prototyping.

An important part of the CLI are cli-plugins. They can modify the internal webpack configuration and inject commands to the vue-cli-service. A great example is a @vue/cli-plugin-typescript: when you invoke it, it adds a tsconfig.json to your project and changes App.vue to have types, so you don't need to do it manually.

Plugins are very useful and there are a lot of them today for different cases - GraphQL + Apollo support, Electron builder, adding UI libraries such as Vuetify or ElementUI... But what if you want to have a plugin for some specific library and it doesn't exist? Well, it was my case πŸ˜…... and I decided to build it myself.

In this article we will be building a vue-cli-plugin-rx. It allows us to add a vue-rx library to our project and get RxJS support in our Vue application.

πŸŽ›οΈ Vue-cli plugin structure

First of all, what is a CLI plugin? It's just an npm package with a certain structure. Regarding docs, it must have a service plugin as its main export and can have additional features such as a generator and a prompts file.

For now it's absolutely unclear what is a service plugin or generator, but no worries - it will be explained later!

Of course, like any npm package, CLI plugin must have a package.json in its root folder and it would be nice to have a README.md with some description.

So, let's start with the following structure:

.
β”œβ”€β”€ README.md
β”œβ”€β”€ index.js      # service plugin
└── package.json

Now let's have a look at optional part. A generator can inject additional dependencies or fields into package.json and add files to the project. Do we need it? Definitely, we want to add rxjs and vue-rx as our dependencies! More to say, we want to create some example component if user wants to add it during plugin installation. So, we need to add either generator.js or generator/index.js. I prefer the second way. Now the structure looks like this:

.
β”œβ”€β”€ README.md
β”œβ”€β”€ index.js      # service plugin
β”œβ”€β”€ generator
β”‚   └── index.js  # generator
└── package.json

One more thing to add is a prompts file: I wanted my plugin to ask if user wants to have an example component or not. We will need a prompts.js file in root folder to have this behavior. So, a structure for now looks the following way:

β”œβ”€β”€ README.md
β”œβ”€β”€ index.js      # service plugin
β”œβ”€β”€ generator
β”‚   └── index.js  # generator
β”œβ”€β”€ prompts.js    # prompts file
└── package.json

βš™οΈ Service plugin

A service plugin should export a function which receives two arguments: a PluginAPI instance and an object containing project local options. It can extend/modify the internal webpack config for different environments and inject additional commands to vue-cli-service. Let's think about it for a minute: do we want to change webpack config somehow or create an additional npm task? The answer is NO, we want just to add some dependencies and example component if necessary. So all we need to change in index.js is:

module.exports = (api, opts) => {}

If your plugin requires changing webpack config, please read this section in official Vue CLI docs.

πŸ› οΈ Adding dependencies via generator

As mentioned above, CLI plugin generator helps us to add dependencies and to change project files. So, first step we need is to make our plugin adding two dependencies: rxjs and vue-rx:

module.exports = (api, options, rootOptions) => {
  api.extendPackage({
    dependencies: {
      'rxjs': '^6.3.3',
      'vue-rx': '^6.0.1',
    },
  });
}

A generator should export a function which receives three arguments: a GeneratorAPI instance, generator options and - if user creates a project using certain preset - the entire preset will be passed as a third argument.

api.extendPackage method extends the package.json of the project. Nested fields are deep-merged unless you pass { merge: false } as a parameter. In our case we're adding two dependencies to dependencies section.

Now we need to change a main.js file. In order to make RxJS work inside Vue components, we need to import a VueRx and call Vue.use(VueRx)

First, let's create a string we want to add to the main file:

let rxLines = `\nimport VueRx from 'vue-rx';\n\nVue.use(VueRx);`;

Now we're going to use api.onCreateComplete hook. It is called when the files have been written to disk.

  api.onCreateComplete(() => {
    // inject to main.js
    const fs = require('fs');
    const ext = api.hasPlugin('typescript') ? 'ts' : 'js';
    const mainPath = api.resolve(`./src/main.${ext}`);
};

Here we're looking for the main file: if it's a TypeScript project, it will be a main.ts, otherwise it will be a main.js file. fs here is a File System.

Now let's change file content

  api.onCreateComplete(() => {
    // inject to main.js
    const fs = require('fs');
    const ext = api.hasPlugin('typescript') ? 'ts' : 'js';
    const mainPath = api.resolve(`./src/main.${ext}`);

    // get content
    let contentMain = fs.readFileSync(mainPath, { encoding: 'utf-8' });
    const lines = contentMain.split(/\r?\n/g).reverse();

    // inject import
    const lastImportIndex = lines.findIndex(line => line.match(/^import/));
    lines[lastImportIndex] += rxLines;

    // modify app
    contentMain = lines.reverse().join('\n');
    fs.writeFileSync(mainPath, contentMain, { encoding: 'utf-8' });
  });
};

What is happening here? We're reading the content of the main file, breaking it into lines and reverting their order. Then, we search the first line with an import statement (it will be the last one after the second reverting) and add our rxLines there. After this, we reverse the array of lines and save the file.

πŸ’» Testing cli-plugin locally

Let's add some information about our plugin in package.json and try to install it locally to test. The most important information usually is a plugin name and a version (these fields will be required when publishing the plugin to npm), but feel free to add more info! The full list of package.json fields could be found here. Below is my file:

{
  "name": "vue-cli-plugin-rx",
  "version": "0.1.5",
  "description": "Vue-cli 3 plugin for adding RxJS support to project using vue-rx",
  "main": "index.js",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/NataliaTepluhina/vue-cli-plugin-rx.git"
  },
  "keywords": [
    "vue",
    "vue-cli",
    "rxjs",
    "vue-rx"
  ],
  "author": "Natalia Tepluhina <my@mail.com>",
  "license": "MIT",
  "homepage": "https://github.com/NataliaTepluhina/vue-cli-plugin-rx#readme"
}

Now it's time to check how our plugin works! To do so, let's create a simple vue-cli-powered project:

vue create test-app

Go to the project folder and install our newly created plugin:

cd test-app
npm install --save-dev file:/full/path/to/your/plugin

After plugin is installed, you need to invoke it:

vue invoke vue-cli-plugin-rx

Now, if you try to check the main.js file, you can see it's changed:

import Vue from 'vue'
import App from './App.vue'
import VueRx from 'vue-rx';

Vue.use(VueRx);

Also, you can find your plugin in devDependencies section of your test app package.json.

πŸ“‚ Creating a new component with generator

Great, a plugin works! It's time to extend its functionality a bit and make it able to create an example component. Generator API uses a render method for this purpose.

First, let's create this example component. It should be a .vue file located in project src/components folder. Create a template folder inside the generator one and then mimic this whole structure inside of it:

tree screenshot

Your example component should be...well, just a Vue single-file component! I won't dive into RxJS explanations in this article, but I created a simple RxJS-powered click counter with 2 buttons:

Rx Example

Its source code could be found here.

Now we need to instruct our plugin to render this component on invoke. To do so, let's add this code to generator/index.js:

api.render('./template', {
  ...options,
});

This will render the whole template folder. Now, when plugin is invoked, a new RxExample.vue will be added to src/components folder.

I decided not to overwrite an App.vue and let users add an example component on their own. However, you can replace parts of existing files, see examples in docs

⌨️ Handling user choices with prompts

What if user doesn't want to have an example component? Wouldn't it be wise to let users decide on this during plugin installation process? That's why prompts exist!

Previously we've created prompts.js file in the plugin root folder. This file should contain an array of questions: every question is an object with certain set of fields such as name, message, choices, type etc.

Name is important: we will use it later in generator to create a condition for rendering an example component!

Prompt can have different types but the most widely used in CLI are checkbox and confirm. We will use the latter to create a question with yes/no answer.

So, let's add our prompt code to prompts.js!

module.exports = [
  {
    name: `addExample`,
    type: 'confirm',
    message: 'Add example component to components folder?',
    default: false,
  },
];

We have an addExample prompt which will ask user if he/she would like to add a component to components folder. Default answer is No.

Let's go back to the generator file and do some fixes. Replace out api.render call with

if (options.addExample) {
    api.render('./template', {
      ...options,
    });
}

We're checking if addExample has a positive answer and, if so, the component will be rendered.

Don't forget to reinstall and test you plugin after each change!

πŸ“¦ Make it public!

Important note: before publishing your plugin please check if its name matches the pattern vue-cli-plugin-<YOUR-PLUGIN-NAME>. This allows your plugin to be discoverable by @vue/cli-service and installable via vue add.

I also wanted my plugin to have a nice appearance in Vue CLI UI, so I added a description, tags and repository name to package.json and created a logo. Logo picture should be named logo.png and placed in the plugin root folder. As a result, my plugin looks this way in UI plugins list:

vue-rx in UI

Now we're ready to publish. You need to be registered an npmjs.com and obviously you should have npm installed.

To publish a plugin, go to the plugin root folder and type npm publish in the terminal. VoilΓ , you've just published an npm package!

At this moment you should be able to install a plugin with vue add command. Try it!

Of course, the plugin described in this article is very basic, but I hope my instructions will help someone to start with cli-plugins development.

What kind of CLI plugin you're missing? Please share you ideas in comments πŸ€—

πŸ’– πŸ’ͺ πŸ™… 🚩
n_tepluhina
Natalia Tepluhina

Posted on October 15, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related