How to create a Universal Library for Vue 2 & 3
Tags: vue, universal, library, tutorial
Published: 2021-01-25 15:34:00


As you probably know by now, last September **Evan You** announced the new version of Vue (Vue 3.0 or "One Piece" for friends) during the Vue.js Global Event - Official release [here](https://github.com/vuejs/vue-next/releases/tag/v3.0.0?ref=madewithvuejs.com).

The hype for upgrading code to the latest version of Vue exploded and everyone (including me) was eager to start. But they are breaking changes, especially on the global API, forcing library/plugin authors to migrate their code to support the new version and the **Composition API**. If you want to understand better why I wrote an article on how to do the migration from 2.x to 3.x here - [How to migrate your library from Vue 2.x to Vue 3.x](https://alvarosaburido.dev/blog/how-to-migrate-your-library-from-vue2-to-vue3)

As an author of a Vue library, I have to say that the migration wasn't an easy job, imitating what major libraries did: separating the support for each the targeting version in separate `branches` and `tags` (`main` for vue 2.x and `next` for vue 3.x) or even having a separate repo to ensure better code isolation.

As Vue.js core member [@antfu](https://github.com/antfu) (Anthony Fu) explains in this [post](https://antfu.me/posts/make-libraries-working-with-vue-2-and-3/):

> The drawback of this is that you will need to maintain two codebases that double your workload. For small scale libraries or new libraries that want to support both versions, doing bugfix or feature supplements twice is just no ideal. I would not recommend using this approach at the very beginning of your projects.

It's possible to achieve this by using a developing tool that the same @antfu created called [Vue-demi](https://github.com/antfu/vue-demi).

So if you are interested to learn how to create a universal library/plugin for both versions of Vue, this article is for you.

## Create base setup

Let's begin by creating a new project using [vue-cli](https://github.com/vuejs/vue-cli) prompt.

```
vue create vue-universal-lib
```

Be sure you select the **3.x version** for Vue, and the rest I leave it to your preferences, but I strongly suggest you use the same options as I describe here to be on the same page:
![Vue-cli prompt](https://res.cloudinary.com/alvarosaburido/image/upload/v1609779280/blog/How%20to%20create%20an%20Universal%20Library%20for%20Vue%202%20and%203/Screenshot_2021-01-04_at_17.54.22_jyg8gn.png)
Options selected:

- Babel
- Typescript
- Linter
- Use class-style component syntax **No**
- Use Babel alongside TypeScript **Yes**
- Pick a linter: **ESLint + Prettier**

After some seconds we will have a basic structure to start with. You probably need to get rid of some stuff like the `App.vue` and `main.ts` since we mainly are going to work with an `index.ts` file.

## Find a purpose

Sounds epic right? Fun apart find a necessity, some functionality often used in Web development that you want to implement in Vue and make it reusable, something that you think will bring value being a library/plugin.

![find a purpose](https://media.giphy.com/media/Fsn4WJcqwlbtS/source.gif)

For this tutorial sake, we will create a simple library that allows you **to animate numbers like a counter**, similar to this:

![Jquery number animation](https://i1.wp.com/codemyui.com/wp-content/uploads/2016/07/simple-jquery-number-counter.gif?fit=880%2C440&ssl=1)

This type of component is often used in landing pages to show KPIs.

## Hands dirty

First of all, let's create the `counter-number` component under `src/components/CounterNumber.ts` using `defineComponent`.

```typescript
import { ref, defineComponent, h } from 'vue';

export const CounterNumber = defineComponent({
  name: 'Awesome',
  props,
  setup(props, ctx) {
    const value = ref(640);

    return () =>
      h(
        'span',
        {
          class: 'counter-number',
        },
        value,
      );
  },
});
```

For the moment let's leave it as a presentational component without the animation, later we will add the functionality through a composable function to take advantage of Vue3's [Composition API](https://v3.vuejs.org/api/composition-api.html).

You might also notice there is no template for the component in here, the `setup` function returns a render function with a `<span>` element holding the counter value. That's intended and will be explained in the [Caveates](#caveats) section of the post.

For demo purposes leave out a `main.ts` and the `App.vue` to test the new component using `npm serve`.

## Plugin installation

For creating the plugin itself create a `src/index.ts`:

```typescript
import { App, inject, InjectionKey } from 'vue';
import { CounterNumber } from './components/CounterNumber';

export interface VueCounterOptions {
  theme: string;
}

export interface VueCounterPlugin {
  options?: VueCounterOptions;
  install(app: App): void;
}

export const VueCounterPluginSymbol: InjectionKey<VueCounterPlugin> = Symbol();

export function VueCounterPlugin(): VueCounterPlugin {
  const VueCounterPlugin = inject(VueCounterPluginSymbol);
  if (!VueCounterPlugin) throw new Error('No VueCounterPlugin provided!!!');

  return VueCounterPlugin;
}

export function createVueCounterPlugin(
  options?: VueCounterOptions,
): VueCounterPlugin {
  const plugin: VueCounterPlugin = {
    options,
    install(app: App) {
      app.component('vue-counter', CounterNumber);
      app.provide(VueCounterPluginSymbol, this);
    },
  };

  return plugin;
}
```

Let's break this down into parts, the function `createVueCounterPlugin` will allow you to install the plugin via the `install` method when using `createApp.use()` in your app.

This will add to the `app` instance all the components, properties of your library like you see above with `app.component('vue-counter', CounterNumber);`

To get most of the Composition API and be able to inject into your library components things like `options` or `utilities` we create a **Plugin Symbol** to be used along with `app.provide` in the `install` method where we pass the createVueCounterPlugin itself as a parameter. This might look complicated at the moment, but it's the standard way:

```typescript

// index.ts

...
export const VueCounterPluginSymbol: InjectionKey<VueCounterPlugin> = Symbol();

export function VueCounterPlugin(): VueCounterPlugin {
  const VueCounterPlugin = inject(VueCounterPluginSymbol);
  if (!VueCounterPlugin) throw new Error('No VueCounterPlugin provided!!!');

  return VueCounterPlugin;
}

...

```

---

To install the plugin and test it, go to your `src/main.ts`:

```typescript
import { createApp } from 'vue';
import App from './App.vue';
import './assets/styles/main.css';

import { createVueCounterPlugin } from './';

const VueCounterPlugin = createVueCounterPlugin();

createApp(App).use(VueCounterPlugin).mount('#app');
```

If you like to pass options to your plugin you can do it like this

```javascript
const VueCounterPlugin = createVueCounterPlugin({ theme: 'light' });
```

The magic behind what we did is that using `app.provide` in the plugin install method is that we can inject the plugin options as a dependency later.

Now let's add the `CounterNumber` component into the `src/App.vue`.

```vue
// App.vue

<template>
  <h2 class="font-bold text-2xl mb-8 text-gray-600">
    Vue Counter animation
  </h2>
  <div
    class="card bg-gray-100 rounded-xl p-8 auto shadow-lg mx-auto w-1/3 text-indigo-400 font-bold text-xl"
  >
    <vue-counter />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'App',
});
</script>
```

If you are curious about the utility classes I used here, is the awesome [TailwindCSS](https://tailwindcss.com/) which I love for doing quick prototypes. You can install it also by following this [guide](https://tailwindcss.com/docs/guides/vue-3-vite). Just make sure you add those dependencies as `devDependencies` to your `package.json` or they will be included in your library bundle.

Let's see how it looks on the browser with `npm run serve`

![npm run serve](https://res.cloudinary.com/alvarosaburido/image/upload/v1610039382/blog/How%20to%20create%20an%20Universal%20Library%20for%20Vue%202%20and%203/Screenshot_2021-01-07_at_18.07.54_yumfca.png)

## Animation and composition

Looks beautiful, but needs more magic. Let's add the easing animation for the counter. To achieve a smooth animation, we will be using a library called [anime.js](https://animejs.com/), which is really lightweight and offers and plain simple API.

We could add the logic directly on the `CounterNumber` component, but since we talked before about **Composition API** let's use it for this purpose.

Create a `useCounter.ts` file under `src/composables` and export a function called `useCounter` like this:

```typescript
import { ref } from 'vue';

import anime from 'animejs/lib/anime.es.js';

export function useCounter() {
  const count = ref(0);
  const counter = {
    value: 0,
  };

  anime({
    targets: counter,
    duration: 2000, // 2000ms
    value: 640,
    easing: 'easeOutQuad',
    update: () => {
      count.value = Math.round(counter.value);
    },
  });

  return {
    count,
  };
}
```

We import a factory function called 'anime' from 'animejs/lib/anime.es.js' and we pass a target (in this case an obj containing a `ref` with the value to be animated).

The `anime()` function accepts a lot of parameters to customize the behavior of the animation such as **duration**, **delay**, **easing**, and callbacks like an **update** that triggers every time the animation updates the target object. The interesting thing is that you can pass as property the same property you want to animate, in this case `value`, in the example above will go from 0 to 640. For more info about the **animejs API** check the [docs](https://animejs.com/documentation/#JSobjProp)

Go back to your `CounterNumber.ts` component and get the use the `count.value` inside the `span` like this:

```typescript
export const CounterNumber = defineComponent({
  name: 'Awesome',
  props,
  setup(props, ctx) {
    const { count } = useCounter();

    return () =>
      h(
        'span',
        {
          class: 'counter-number',
        },
        count.value,
      );
  },
});
```

Now go back to the browser and refresh to see how the counter goes from **0** to **640** in 2 seconds.

## Make it customizable

At the moment, all values are hardcoded, but since we are doing a library, these parameters for the animation should be customizable and therefore passed as props to the component and down to the composition function.

First, let's add some props that make sense:

```typescript

// src/components/Counternumber

const props = {
  from: {
    type: [Number, String],
    default: 0,
  },
  to: {
    type: [Number, String],
    required: true,
    default: 0,
  },
  duration: {
    type: Number,
    default: 1000, // Duration of animation in ms
  },
  easing: {
    type: String,
    default: 'easeInOutQuad',
  },
  delay: {
    type: Number,
    default: 0, // Delay the animation in ms
  },
};

export const CounterNumber = defineComponent({
  name: 'Awesome',
  props,
  setup(props, ctx) {
    const { count } = useCounter(props);
...
  },
});
```

Make sure you pass the props to the `useCounter(props)` function;

Go to `App.vue` and create some variables to pass to the component as props:

```typescript
<template>
  <h2 class="font-bold text-2xl mb-8 text-gray-600">Vue Counter animation</h2>
  <div
    class="card bg-gray-100 rounded-xl p-8 auto shadow-lg mx-auto w-1/3 text-indigo-400 font-bold text-xl"
  >
    <vue-counter :from="0" :to="640" :duration="3000" :delay="2000" />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'App',,
});
</script>
```

Finally, go back to `useCounter.ts` and pass the props to the `anime` instance

```typescript
import { ref } from 'vue';

import anime from 'animejs/lib/anime.es.js';

export function useCounter(props: any, emit: any) {
  const count = ref(0);
  const counter = {
    value: props.from,
  };

  anime({
    targets: counter,
    duration: props.duration,
    value: props.to,
    delay: props.delay,
    easing: props.easing || 'linear',
    update: () => {
      count.value = Math.round(counter.value);
    },
  });

  return {
    count,
  };
}
```

Of course, we would need to add more code to make it create a new instance of the anime object every time a prop change, but for the scope of the article is more than enough.

![Vue Counter Animation](https://res.cloudinary.com/alvarosaburido/image/upload/v1610126895/blog/How%20to%20create%20an%20Universal%20Library%20for%20Vue%202%20and%203/vue-counter-animation_pvdqmv.gif)

## Make it universal

So great, we have our awesome library ready, at the moment, is only usable on a project with for **Vue 3**, how can we achieve an isomorphic installation?

That's where `vue-demi` comes to the rescue.

```bash
npm i vue-demi
# or
yarn add vue-demi
```

Add `vue` and `@vue/composition-api` to your plugin's peer dependencies to specify what versions you support.

```json
// package.json

{
  "dependencies": {
    "vue-demi": "latest"
  },
  "peerDependencies": {
    "@vue/composition-api": "^1.0.0-beta.12",
    "vue": "^2.6.11 || >=3.0.5"
  }
}
```

Now comes the important part 📝, to take notes on it: replace all the **imports** coming from `vue` to `vue-demi`, like so:

```typescript
import { defineComponent, ref } from 'vue';
```

Will become:

```typescript
import { defineComponent, ref } from 'vue-demi';
```

The library will redirect to `vue@2` + `@vue/composition-api` or `vue@3` based on users' environments.

That's powerful.

## Build config

You can build your plugin bundle in so many different ways, webpack, vue-cli (webpack also), parser, rollup, etc. It's up to you, but I really recommend using [rollup.js](https://rollupjs.org/guide/en/), is a great module bundler, really easy to get into, and is used in most of the major Vue plugins out there, such as [Vue Router](https://github.com/vuejs/vue-router-next).

```bash
yarn add rollup rollup-plugin-vue rollup-plugin-typescript2 rollup-plugin-terser @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-replace -D
```

Also, we will need to tweak a little bit the configuration so it externalizes `vue-demi` instead of `vue` and set it as a global at the build moment. Because the `rollup.config.js` is quite large, here is the [link](https://github.com/alvarosaburido/vue-universal-lib-example/blob/main/rollup.config.js) to it at the example repo.

In the method `createConfig` make sure you have `vue-demi` set in the property globals like this:

```javascript
// rollup.config.js
...
output.globals = { 'vue-demi': 'VueDemi' };
...
const external = ['vue-demi'];

```

Finally, let's add a `script` in the `package.json` and the paths for the package builds:

```json
// package.json

"scripts": {
  "build": "rollup -c rollup.config.js",

}
"main": "dist/vue-universal-lib.cjs.js",
"browser": "dist/vue-universal-lib.esm.js",
"unpkg": "dist/vue-universal-lib.global.js",
"jsdelivr": "dist/vue-universal-lib.global.js",
"module": "dist/vue-universal-lib.esm-bundler.js",
"types": "dist/vue-universal-lib.d.ts",
```

## Caveats

Of course, is not all roses 🌹 and unicorns 🦄, the use case of `vue-demi` is rather for vue plugins that don't rely too much on rendering components because Vue 2 and Vue 3 render functions are quite different and the breaking changes between both, i.e. `v-model` on a component expecting differently named events in Vue 2 vs 3 (ìnput vs `update:modelValue`).

That's why we used a render function for our component definition and a `.ts` file instead of a `.vue` file. For this example library, it will not affect the end result but it's something you need to take into consideration.

One way to possibly adapt breaking changes in your lib component would be the use of extra APIs from `Vue Demi` to help distinguishing users' environments and to do some version-specific logic.

`isVue2` `isVue3`

```typescript
import { isVue2, isVue3 } from 'vue-demi';

if (isVue2) {
  // Vue 2 only
} else {
  // Vue 3 only
}
```

That being said I hope this article was illustrative enough on the journey of creating a universal plugin for Vue. Let me hear your thoughts and questions below.

Happy coding! 😎

![cat coding](https://media.giphy.com/media/LmNwrBhejkK9EFP504/giphy.gif)