Plugin architecture with Meteor
Tags: javascript, meteor, tutorial
Published: 2021-01-20 14:54:00

Writing packages for Meteor is easy and straight forward. However, if you want to allow your users to extend their application on their own, you usually have to implement some kind of [plugin architecture](https://en.wikipedia.org/wiki/Plug-in_(computing)).

By doing so, you can distinctively control what functionality users can add within the limits you define. 

In this tutorial we want to focus on a potential approach to load plugins from packages without importing them directly but using a dynamic mechanism:

- no manual configuration of settings should be required
- no manual imports of the plugin should be required
- plugin package added -> plugin available
- plugin package removed -> plugin not available

Furthermore there should be a very important contstraint:

- no plugin should be added to the initial client bundle, unless loaded by the `plugin-loader` (imagine 100 plugins loaded all at application startup -> super slow)

## A minimal example project

For this tutorial we will create a minimal example project. I am using the defaults here, including Blaze (Meteor's default frontend). This, however, shouldn't prevent you from picking your favourite frontend as the proposed plugin architecture will (and should!) work independently from it.

### Preparations - Overview of the architecture

Our example will consists of three main entities:

- Meteor project `"plugin-example"`
- Package `"plugin-loader"`
- Package `"hello-plugin"`

Their relation is fairly simple: The plugins will use the plugin-loader to "register" themselves, while the Meteor project uses the plugin-loader to load the plugins via dynamic import. Thus, the `plugin-loader` package has to be a package, shared by the other two.

**We want to keep things simple.** Therefore, a plugin will consist of the following minimal interface:

```javascript
{
  name: String,
  run: () => String
}
```

Now if you haven't installed Meteor yet, you may [install it now](https://www.meteor.com/developers/install), which takes only a minute or two.

### Step 1 - Create project and packages

Creating the project and the packages is done in no time:

```bash
$ meteor create plugin-example
$ cd plugin-example
$ meteor npm install
$ mkdir -p packages
$ cd packages
$ meteor create --package plugin-loader
$ meteor create --package hello-plugin
```

Once you have created them you need to add both packages to the project:

```bash
$ cd ..
$ meteor add plugin-loader hello-plugin
```

Now everything is setup and we can start implementing the `plugin-loader`, first.

### Step 2 - Implement the `plugin-loader`

The plugin loader itself is not very complicated, either. It's only functionality defines as the following:

- register a plugin by a given name and load-function, where the name distincs the plugin from others and the load function will actually load the plugin into the host application
- load all plugins by executing all registered load functions and return an array of all loaded plugins 

For implementation we use a simple Map to store the data and provide only two functions for access:

<small>*packages/plugin-loader/plugin-loader.js*</small>
```javascript
export const PluginLoader = {}

/** internal store of load functions **/
const plugins = new Map()

/**
 * Add a plugin to the loader.
 * @param key {String} the plugin name, prevent duplicates
 * @param load {aync Function} imports the actual plugin
 */
PluginLoader.add = (key, load) => {
  plugins.set(key, load)
}

/**
 * Load all registered plugins. Could be extended by a filter.
 * @return {Promise} a promise that resolves to an array of all loaded plugins
 */
PluginLoader.load = () => {
  const values = Array.from(plugins.values())
  plugins.clear()
  return Promise.all(values.map(fct => fct()))
}
```

That's it for the plugin loader. You can keep the other files in the package as they are and move to the next step.

### Step 3 - Implement the plugin

This is the most critical part, since the correct utilization of the plugin loader is presumed in order to not load the plugins into the initial client bundle. Keep focused as I will explain things after the steps in detail.

Let's start off with our plugin itself, which should simply just return some hello-message when called:

<small>*packages/hello-plugin/hello-plugin.js*</small>

```javascript
const HelloPlugin = {}

HelloPlugin.name = 'helloPlugin'

HelloPlugin.run = function () {
  return 'Hello from a plugin'
}

;(function () {
  // if you see this line at startup then something went wrong
  console.info('plugin loaded')
})()

module.exports = HelloPlugin
```

Nothing fancy but now we need to **create a new file**, which will register the plugin to the loader:

<small>*packages/hello-plugin/register.js*</small>

```javascript
import { PluginLoader } from 'meteor/plugin-loader'

PluginLoader.add('helloPlugin', async function () {
  // await import(...) import other dependencies
  // from this package, if necessary
  return import('./hello-plugin')
})
```

This actually registers not the plugin but an async function that itself is used to call the dynamic import of the plugin (and other files from this package, if necessary).

**Caution:** If you directly use `import('./hello-plugin')` it will immediately import the plugin, which is not what we want here.

Finally in order to "automagically" register the plugin, we need to make a small change in the `package.js` file so it looks like the following:

<small>*packages/hello-plugin/package.js*</small>
```javascript
Package.onUse(function (api) {
  api.versionsFrom('1.12.1')
  api.use('ecmascript')
  api.use('plugin-loader')
  api.addFiles('register.js')
})
```

This works, because `api.addFiles` not only adds the file to the initial client bundle, it also makes sure the code in it is executed when the client starts. However, since we removed the `api.mainModule` call and have no other reference to the `hello-plugin.js` besides the dynamic import, this file will not be added until the loader loads it.

Now we can integrate both packages into our application in the next step.

### Step 4 - Load the plugin on demand

To keep things minimal we will only focus on the client here. Therefore, we will only do changes in the `client/` folder.

Based on the initial `main.js` file we import the plugin loader and create some reactive variable to indicate, whether we have loaded plugins, or not.

<small>*client/main.js*</small>
```javascript
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { PluginLoader } from 'meteor/plugin-loader'
import './main.html';

const loadedPlugins = new Map()

Template.hello.onCreated(function helloOnCreated() {
  const instance = this
  instance.loaded = new ReactiveVar(false)
})

Template.hello.helpers({
  plugins () {
    return Array.from(loadedPlugins.values())
  },
  loaded () {
    return Template.instance().loaded.get()
  }
})

...
```

Then we add a button, on whose action we actually load the plugins using the loader:

<small>*client/main.js*</small>
```javascript
...

Template.hello.events({
  'click .load-button': async function (event, instance) {
    const allPlugins = await PluginLoader.load()
    
    allPlugins.forEach(plugin => {
      loadedPlugins.set(plugin.name, plugin)
    })

    instance.loaded.set(true)
  }
})
```

Since `PluginLoader.load` returns a `Promise<Array>` (via `Promise.all`) we can use `async/await` to keep the code readable.

When all plugins have been loaded we can simply store them in a data structure (like a Map, used in the example) and then set the reactive variable `loaded` to `true` so it will cause the Template to render our plugins.

Note, that you can't directly store the plugins in a reactive variable, since they may loose their functions to work.

Finally, the Template is nothing fancy and should look like the following:

<small>*client/main.html*</small>
```html
<head>
  <title>plugin-example</title>
</head>

<body>
  <h1>Plugins example</h1>

  {{> hello}}
</body>

<template name="hello">
    {{#if loaded}}
        {{#each plugin in plugins}}
            {{plugin.name}}: {{plugin.run}}
        {{/each}}
    {{else}}
        <button class="load-button">Load plugins</button>
    {{/if}}
</template>
```
All done and ready to start. :rocket:

## Step 5 - running the code

In your project you can enter the `meteor` command to run the code:

```bash
$ cd /path/to/plugin-example
$ meteor
```

Then open `http://localhost:3000/` and you should see something like this:

![load plugin button screenshot](https://dev-to-uploads.s3.amazonaws.com/i/lqsqlvh621ww6ff0hxoo.png)

At this point your browser console (F12) should not!!! have printed `"plugin loaded"`

Now click the button and load the plugin. You should see now the plugin output:

![loaded plugin screenshot](https://dev-to-uploads.s3.amazonaws.com/i/khi7pubo799en8rfae9m.png)

Additionally in your browser console there should now the `"plugin loaded"` have been printed.

:tada: Congratulations, you created an initial foundation for a simple plugin architecture in Meteor.

## Summary and outlook

With this tutorial we have set the foundation of writing pluggable software by using a simple plugin-loader mechanism.

In future tutorials we could focus on the plugin interface, how it interacts with the host application and how we can make use of some of Meteor's core features (Mongo, Authentication, Methods, Pub/Sub) to ease up plugin-development.

<hr>
I regularly publish articles here on dev.to about **Meteor** and **JavaScript**. If you like what you are reading and want to support me, you can [send me a tip via PayPal](https://paypal.me/kuesterjan).

You can also find (and contact) me on [GitHub](https://github.com/jankapunkt/), [Twitter](https://twitter.com/kuester_jan) and [LinkedIn](https://www.linkedin.com/in/jan-kuester/).

Keep up with the latest development on Meteor by visiting [their blog](https://blog.meteor.com/?utm_source=jkuester&utm_medium=online&utm_campaign=Q2-2022-Ambassadors) and if you are the same into Meteor like I am and want to show it to the world, you should check out the [Meteor merch store](https://shop.meteor.com/?utm_source=jkuester&utm_medium=online&utm_campaign=Q2-2022-Ambassadors).