Let’s write a Vue.js 3 Plugin with TypeScript from scratch – Part 1

I want to write a Plugin for Vue.js 3, and I intend to write it with TypeScript. Normally, this is relative easy, there are plenty of tutorials and article about this topic. But one part was always missing: A plugin with custom Single File Components and also written in TypeScript.

This article consists of two parts:

Write the plugin

Writing a new plugin consists of the following few steps

  1. Initialize a new project
  2. Write the actual Plugin
  3. Configure TypeScript
  4. Configure rollup.js

Initialize a the project

mkdir my-plugin
cd my-plugin
yarn init -y

Now we have a new, but empty project. Let’s add a few packages to our devDependencies

  1. Vue So we can create our custom components in our plugin
  2. TypeScript (Will compile our *.ts and *.vue files to vanilla JavaScript)
  3. rollup.js with plugins for Vue & TypeScript (“Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application.” – From the official documentation at rollupjs.org) See it as an alternative to Webpack.
yarn add -D vue typescript rollup rollup-plugin-typescript2 rollup-plugin-vue rollup-plugin-clear

Now is a good moment, to initialize a new git repository. This step is optional, but highly recommended

git init
echo "node_modules" > .gitignore
git add .
git commit -m"init"

Write the plugin

Our plugin will be simple. It consists of a single component with a static text (Hello World) and with a configurable text color. The color can be set while installing the plugin in the example project.

The Plugin will be registered globally as this.$coloredText

Our Single File Component (SFC)

<template>
  <h1 :style="{ color: $coloredText.textColor }">
    <slot></slot>
  </h1>
</template>
<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  name: "Headline"
})
</script>

The definition of our Plugin

This is the definition of the actual plugin, used later in your Vue.js app.

export interface ColoredText
{
    textColor: string
}

The definition of our Plugin Options

This will define all the options of your plugin.

export interface ColoredTextOptions {
    color: string
}

The Plugin

This file contains the install function, needed by Vue.js to register the plugin

import { App, Plugin } from 'vue';
import Headline from './components/Headline.vue'
import { ColoredText } from "./types/ColoredText";
import { ColoredTextOptions } from './types/ColoredTextOptions';

// Used to create a new ColoredText. "options" will be whatever you add later to "app.use(ColoredTextPlugin, options);"
const createColoredText = (options: ColoredTextOptions): ColoredText => {
    return {
        textColor: options.color,
    }
}

// The Install function used by Vue to register the plugin
export const ColoredTextPlugin: Plugin = {
    install(app: App, options: ColoredTextOptions) {
        // makes ColoredText available in your Vue.js app as either "$this.coloredText" (in your Source) or "{{ $coloredText }}" in your templates
        app.config.globalProperties.$coloredText = createColoredText(options)
        // register Headline as a global component, so you can use it wherever you want in your app
        app.component('Headline', Headline)
    }
}

Let’s combine everything together. This will be the entry point to our package

import Headline from './components/Headline.vue'
import { ColoredTextOptions } from './types/ColoredTextOptions';
import { ColoredText } from './types/ColoredText';
import { ColoredTextPlugin} from './ColoredTextPlugin'

export { Headline, ColoredText, ColoredTextOptions, ColoredTextPlugin }

Define build process

Configure TypeScript

We want our plugin to be compiled to esnext, our output directory should be ./dist and all *.ts and *.vue

Disclaimer: I am by no means a TypeScript expert, so let me please know, if I missed a crucial configuration, but this works for me

{
  "compilerOptions": {
    /* Set the JavaScript language version for emitted JavaScript and include 
       compatible library declarations. */
    "target": "esnext",
    /* Specify what JSX code is generated. */
    "jsx": "preserve",
    /* Specify what module code is generated. */
    "module": "esnext",
    /* Specify the root folder within your source files. */
    "rootDir": "./src",
    /* Specify how TypeScript looks up a file from a given module specifier. */
    "moduleResolution": "node",
    /* Specify the base directory to resolve non-relative module names. */
    "baseUrl": "./src",
    /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
    "declaration": true,
    /* Create sourcemaps for d.ts files. */
    "declarationMap": true,
    /* Create source map files for emitted JavaScript files. */
    "sourceMap": true,
    /* Specify an output folder for all emitted files. */
    "outDir": "./dist",
    /* Emit additional JavaScript to ease support for importing CommonJS modules.
       This enables `allowSyntheticDefaultImports` for type compatibility. */
    "esModuleInterop": true,
    /* Ensure that casing is correct in imports. */
    "forceConsistentCasingInFileNames": true,
    /* Enable all strict type-checking options. */
    "strict": true,
    /* Skip type checking all .d.ts files. */
    "skipLibCheck": true
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue"
  ],
  "exclude": [
    "**/node_modules",
  ]
}

Now comes one important step. We must tell TypeScript how to handle *.vue files

declare module '*.vue' {
  import {defineComponent} from 'vue';
  const component: ReturnType<typeof defineComponent>;
  export default component;
}

Configure rollup.js

Now we must configure rollup.js. This will compile my *.ts files with TypeScript, wraps everything together to a nice compact bundle and saves it to ./dist

import typescript from 'rollup-plugin-typescript2';
import vue from 'rollup-plugin-vue';
import clear from 'rollup-plugin-clear';

export default async function config(args) {
  return {
    input: 'src/index.ts',
    output: {
      dir: 'dist',
      format: 'cjs',
      sourcemap: true,
    },
    plugins: [
      vue(),
      typescript({
        tsconfigOverride: {
          compilerOptions: {
            declaration: true,
          },
          include: null,
        },
      }),
      clear({
        targets: ['./dist'],
      })
    ],
  };
}

Lastly, we must add the path to our compiled src/index.ts and our build scripts to our package.json. Don’t forget to change @your-npm-scope to your actual scope. For details, see here.

{
  "name": "@skuhnow/vue3-plugin-typescript",
  "version": "1.0.6",
  "main": "dist/index.js",
  "license": "MIT",
  "peerDependencies": {
    "vue": "^3.2.20"
  },
  "devDependencies": {
    "rollup": "^2.58.0",
    "rollup-plugin-clear": "^2.0.7",
    "rollup-plugin-typescript2": "^0.30.0",
    "rollup-plugin-vue": "^6.0.0",
    "typescript": "^4.4.4"
  },
  "scripts": {
    "build": "rollup -c",
    "prepublishOnly": "yarn build"
  }
}

Publish to npm

Now we are ready to publish our glorious package to the npm Registry. I assume, you are already registered, if not, it’s not that hard: Creating and publishing scoped public packages

Don’t forget to increase your package version before publishing

To publish just write

npm publish --access public

1 Gedanke zu „Let’s write a Vue.js 3 Plugin with TypeScript from scratch – Part 1“

  1. Thanks
    I have a question, how to resolve this issue. That’s happens because I added to my component

    [!] (plugin rpt2) RollupError: Unexpected token (Note that you need plugins to import files that are not JavaScript)

    Antworten

Schreibe einen Kommentar