——

Creating Plugins

Overview

A Factor plugin is a standard node module that has a few tweaks to make it work elegantly with Factor core. The goal with Factor plugins is to make them "drop-in" which means the following:

  • No code needed for basic use
  • Intelligent, sensible defaults
  • Minimal configuration
  • Easily added and deleted

Luckily, Factor makes all this easy to do using $filters along with the core extensions system.

Note: The easiest way to start a plugin is copy and edit an existing plugin

Defining a Plugin

To start a plugin, create a folder in your local development directory that includes a standard package.json file. This single step defines a NPM module, which is the basis for your plugin.

In your package.json, add a factor property. This is where we'll add Factor specific configuration info and tells Factor it's a plugin.

An example factor property looks like this:

// package.json
{
  "name": "my-factor-plugin",
  "factor": {
    "id": "someId", // Adds main file to Factor as Factor.$someId

    "target": ["app"], // Loads in app env.
    // or
    "target": ["app", "server"], // loads index.js in app & server env.
    // or
    "target": {
      "app": "index", // Loads index.js in app env and server.js in server env
      "server": "server"
    },
    // or advanced
    "target": {
      "app": ["index", "secondFile"], // Advanced case, full control of several files and where they load
      "server": ["server", "index", "somethingElse"]
    },

    "extend": "theme", // or "plugin" ... defaults to "plugin"

    "priority": 100 // Load priority ... defaults to 100. (Lower number is earlier load)
  }
}

Setting Up Development

To setup development of a plugin, you'll need to create a basic "example" app that can be used as the place you'll run the factor dev cli command. Here you'll reference your plugin by adding it as a dependency. There are two ways to do this elegantly:

  • Monorepo: Use Yarn Workspaces and a monorepo to make locally referencing packages a breeze. (We use this approach for Factor and Factor Extend.)
  • file: prefix: it's possible to locally reference modules using the NPM file: prefix discussed here.

Once you've successfully setup your development environment, you should start your "example" app using yarn factor dev. Then you should be able to work on your plugin and see how changes affect your app in real-time.

Loading Extensions

Main Files

A main file is a convention in JS modules that tells the system what file should be loaded when a module is imported into a script. In Factor, the default is index.js but the target attribute gives you fine control over what is loaded and where (discussed below).

Class Pattern

Inside Factor main files, Factor recommends you use a standard class "closure" pattern that makes it easy to access the Factor global and also work with your plugin throughout the Factor system. The pattern looks like this:

// index.js or server.js (main file)
export default Factor => {
  return new (class {
    constructor() {
      // Initialize
    }
  })()
}

Loading in Server vs App Environment

Factor has two key environments: "app" and "server".

  • APP - The app environment consists of a standard Webpack driven VueJS application. This is compiled during build into an optimized "bundle" and is where all your components, routes, CSS, etc live.

  • SERVER - The server environment consists of the CLI and endpoint environments. Endpoints are where trusted actions like API calls (that require private keys) take place, while the CLI is where your application is built and served.

The Problem

Webpack analysis vs Node process

Both these environments run Javascript and Factor goes to lots of effort to make both of these environments work together nicely. However, there are some realities of the underlying software you'll need to be aware of to work effectively (and avoid painful bugs!). These are:

  • Webpack Static Analysis - Webpack does static analysis of all files it sees in its compile path. This means that code that isn't ever technically run in the app environment still gets included in the build. Some NodeJS code is simply not compatible with the browser environment and will throw mysterious errors in your terminal.

  • Node "Long-Running" Process - Your Node process should be considered "long running" meaning info that is stored in memory will last a long time, as opposed to in the browser where everything starts from scratch with every page load. For that reason, server code sometimes needs to be written to accomodate this and avoid "stateful singletons."

While it's often ok to load the same code into both environments, these differences can sometime make it important to separate code into app vs server files. That's why Factor introduces the "target" attribute discussed below.

The Solution

The "Target" Attribute

Configuring the target attribute in package.json tells Factor how it should load main files for an extension. There are two environments: "app" and "server" and they can be set to load the default index.js, a different file for each environment, or many files based on environment.

// package.json
"main": "index.js",
"factor": {
  "target": ["app"] // load index.js only in app environment
  "target": ["app", "server"] // load index.js in both server and app
  "target": {"app": "index", "server": "server"} // load index.js in app, server.js on server
}

Working with Your Plugin

Once you have your plugin loading, you're ready to start working on your plugin. Plugins add or alter functionality to a Factor app in these primary ways:

Filters and Events

Filters provide a standard and versatile way to modify data and functionality of functions elsewhere in Factor. Typically all you need to do is place your filters in the plugins main file and use these to add the functionality you need.

Also, Factor has a special Factor.$events utility that makes it easy to emit or listen for events using the following:

Factor.$events.$on("some-event", eventParams => console.log(eventParams)) // listen
Factor.$events.$emit("some-event", eventParams) // emit

Example: Routes, Components, Events

// index.js
export default Factor => {
  return new (class {
    constructor() {
      this.addRoutes()
      this.addComponents()
      this.events()
    }

    addRoutes() {
      // Takes an array []
      Factor.$filters.add("content-routes", routes => {
        return [
          ...routes,
          {
            path: "/my-route",
            component: () => import("./component-for-my-route")
          }
        ]
      })
    }

    addComponents() {
      // Takes an Object {}
      Factor.$filters.add("components", components => {
        return { ...components, "my-component-name": () => import("./my-component.vue") }
      })
    }

    // listen for events
    events() {
      Factor.$events.$on("some-event", params => {
        console.log("params")
      })
    }

    // emit events
    notify(message) {
      Factor.$events.$emit("notify", message)
    }
  })()
}

Plugin Global Reference

All Factor extensions add a reference to the global Factor variable so that they can be referenced elsewhere directly if needed.

The "id" property you set in your package.json determines the name of the reference. For example, if your id is myEmailExtension then once it's loaded it will be available by reference as Factor.$myEmailExtension everywhere else in your app.

// package.json
"factor": {
    "id": "myEmailPlugin",
    "target": "app"
}

// Inside a component
async myComponentMethod(){
  await Factor.$myEmailPlugin.sendEmail() // send an email or something
}

Handling Multiple References

Handling multiple references per plugin is easy. If the main file is named something other than index.js then the name of the file will be appended to the ID with the first letter capitalized. If loading server.js then that class is reference as Factor.$myIdServer.

Settings and Styles

Plugins are also fully compatible with Factor's native factor-settings and factor-styles systems. Read more about them in the customization doc.

Asset and Convention Guidelines

Once you've created your plugin, you likely want to set it up for distribution and general use. For this purpose, Factor outlines some standards for assets like screenshots, icons and naming that will help your plugin appear professional as well as provide formatting consistency in places your plugin may be listed.

Screenshots

In listings of your plugin, users typically like to scan through some screenshots.

  • Naming: screenshot.jpg, screenshot-2.jpg, screenshot-3.jpg. These will be ordered according to the number (with the default screenshot not needing a number).
  • Sizing The standard screenshot size is 720p: 1280px-by-720px.
  • Quality Conventions
    • You may want to annotate your screenshots to provide context about what exactly does.

Icon

Icons are used in grids, graphics and other places plugins might be 'scanned' for. You'll want to make sure your icon is simple and professional designed.

  • Naming - icon.svg
  • Sizing - Since SVG icons are scaleable all that is required is that the icon is square. We recommend a 64px-by-64px default sizing.
  • Quality Conventions
    • Don't add text to your icon. Text is always provided alongside icons.

Plugin Name

The name of your extension should generally include the word factor-. This way its possible to scan and search for Factor plugins and have your plugin come up.

Naming Ids, Fields and Filters

When naming your plugin, assigning the ID, naming fields and filters and so on make sure you namespace somehow in order to avoid naming collisions with other authors and developers.

  • Don't name your filter/field/id: comments or job
  • Do prefix your filters/fields/id with a namespace or similar: my-plugin-comments, myPluginId

Documenting Your Plugin

Documentation is a critical piece in getting people to use and appreciate software. While Factor provides conventions and context to help people understand your plugin, you should never assume people know even the basics. Thats why we recommend adding the following instructions to your README.md at a minimum:

  • Overview including Purpose and/or motivation behind the plugin
  • Installation
  • Options and Settings
  • Utilities available (factor setup)
  • FAQ or Related documentation and context links

License

All plugins built and distributed for Factor must be compatible with the GPLv2 license.

Distributing

All that is needed to distribute your plugin for use by the public is to publish it as an NPM package.

If you're using a monorepo (the recommended dev setup), then using Lerna may help coordinate the publishing and management of several plugins and themes.