Plugin Module Guide

Guide to building Actinium & Reactium plugin modules.

Overview

One of the compelling reasons for using the Reactium Framework is our robust and powerful plugin architecture. There are a two approaches to creating plugins:

Build-time Plugins

Build-time Plugins extend the functionality of Reactium or Reactium API (Actinium) projects. When downloaded from the Reactium Plugin Registry, the code from a Build-time Plugin is downloaded into your project reactium_modules (or actinium_modules respectively) directory and bundled with your application at build-time.

Build-time plugins only require a package.json in your plugin directory for publishing it to the Reactium Plugin Registry.

# installs Demo site plugin into reactium_modules
npx reactium install @atomic-reactor/reactium-demo

Create A Build-time Plugin

From Reactium root:
npx reactium component

See: Creating A Component for more information on Component Development

Publish A Build-time Plugin

Once you're done building your plugin, publish it to the Reactium Plugin Registry so that it can be installed in other projects.

From plugin directory:
npx reactium publish

You will be prompted to create a package.json if it does not exist in the plugin directory.

The plugin files will then be compressed and uploaded to the Reactium Plugin Registry.

Plugins are version controlled and exercise an Access-control List (ACL) to restrict who has permission to install the plugin. By default, plugins are public. Making your plugin private will restrict access to the ACL.

npx reactium publish
[ARCLI] > Version: (0.0.138)
[ARCLI] > Private (Y/N): Y

You can update your plugin ACL from your Reactium Plugin Registry account.

Install A Build-time Plugin

Once your plugin has been published, it can be installed in any Actinium or Reactium project.

From Actinium root:
npx reactium install @myscope/MyPlugin

Note: If you don't have access to the specified plugin an error message will be displayed.

NPM Build-time Plugin

The last type of build-time plugin for Reactium / Actinium are NPM modules. Much like how at build-time Reactium will look for Domain Driven artifacts in your source directory, it will also look for them in your node_modules directory, albeit with some constraints. NPM build-time modules are constructed much like actinium_modules and reactium_modules, however you will likely need to transpile these modules as ES5 common-js modules, using a tool like babel before publishing. This is an advanced topic, and requires understanding of these tools.

Actinium NPM Build-time Plugin Module

An NPM module containing a plugin.js for Actinium, will be discovered if there is found an NPM module contains an actinium directory with a file named *plugin.js. This file will automatically be loaded as an Actinium plugin module. If found directly in an actinium directory, the following files will be loaded:

  • actinium/*cloud.js - Parse Cloud functions can be defined here

  • actinium/*plugin.js - Actinium plugin file. Register your plugin and hooks here.

  • actinium/*middleware.js - Register express middleware here to be automatically included.

Reactium NPM Build-time Plugin Module

An NPM module containing a directory named reactium-plugin will be searched for any DDD artifact that would ordinary by globbed by the Node/Express server (such as reactium-boot.js), or any resources that would be loaded automatically by the constructed src/manifest.js manifest file:

  • domain.js - define a domain namespace

  • actions.js - exports redux actions for domain

  • actionTypes.js - exports redux action types for domain

  • reducers.js - exports redux reducers for a domain

  • state.js - export default redux initial state for a domain

  • route.js - export React component route(s)

  • services.js - export API services for a domain

  • middleware.js - export redux middleware to be included

  • enhancer.js - export redux enhancer to be included

  • zone.js - export rendering zone component registration(s) for a domain

  • reactium-hooks.js - plugin hooks can be registered here

  • reactium-boot.js - Node/Express bootstrap hooks can be registered here

Run-time Plugins

Run-time Plugins are served to a Reactium Application from an Actinium instance as pre-compiled static assets via the API, or by adding the CSS/js import to your HTML templates (via modified templates or the Server SDK). This allows you to dynamically add components, register hooks, cause component to render in Zone components throughout your application, even if those component are not present in the app at build time. This is particularly useful for code-splitting with separate codebases, or facilitating 3rd party extensions to your application at runtime.

Run-time Plugins are similar to Build-time Plugins except their code is not downloaded into your project and bundled with your application. A separate script tag includes the plugin. This allows you to add functionality to your Reactium application with hooks at runtime.

Another difference is that Run-time Plugins can be turned on/off from the Reactium Admin without the application being rebuilt, if they are served from an Actinium plugin.

Creating a Run-time Plugin involves creating both an Actinium Build-time Plugin and a Reactium Plugin

Create A Run-time Plugin

After you've created your Actinium Build-time plugin create your Reactium Run-time Plugin:

From Reactium root:
npx reactium plugin module

Follow the prompt to name your module (the word "plugin" must not appear in the name), and it will generate boiler plate directory under src/app/component/plugin-src

<module-name>
├── assets
│   └── style
│       └── <module-name>-plugin.scss // styles to be compiled to <module-name>-plugin.css
├── index.js // Main entry for developing your new runtime-features
├── reactium-boot.js // Used for local development only, not used in compiled module
├── reactium-hooks.js // Used for local development only, not used in compiled module
├── reactium-hooks.json // used by arcli plugin local to toggle local dev /testing
├── umd-config.json // Used by webpack to build your runtime module <module-name>.js
└── umd.js // Build-time entry point

In your src/app/component/plugin-src/<module-name>/index.js file, you will have access to certain Reactium framework libraries that will be available as global externals, automatically as es module imports. For example, the following import statements do not bundle anything with your runtime module, but sources them externally:

// These default (alias) and named exports are automatically external
// dependencies when used in runtime plugins
import Reactium, { __, useHookComponent } from 'reactium-core/sdk'; // valid
// import SDK from 'reactium-core/sdk'; // invalid, must use alias "Reactium"
import op from 'object-path';
import _ from 'underscore';

The following modules can be imported without adding any additional weight to your runtime plugins.

  • axios

  • classnames

  • copy-to-clipboard

  • gsap/umd/TweenMax

  • moment

  • object-path

  • prop-types

  • react* (React is alias for defaultexport)

  • react-router-dom

  • redux

  • redux-super-thunk

  • react-dom *(ReactDOM is alias for defaultexport)

  • reactium-core/sdk * (Reactium is alias for default export)

  • semver

  • shallow-equals

  • underscore

  • uuid

  • xss

Important: *Due to the mechanism used by webpack to proved external dependencies, when external ecmascript-modules above contain BOTHdefault export AND one or more "named exports", there will be a mandatory alias for the default export.

For example, you must use Reactium as the default export of reactium-core/sdk, React as the default export of react, etc. You may not import such default exports to a different object name. All named exports can be imported in any way you would ordinarily use them.

Registering Components

One of the most useful features of your runtime plugin is the ability to provide your component or consume external React components from a completely different code bundle (even from a different codebase) at runtime.

In the target application, often the developer will have provided a rendering Zone in the application, that will render unknown (TBD) components, may have declared a theoretical component of a specific name and property set that is not implemented (i.e. left for you to implement in your plugin), or may have registered a component from the codebase that you may use in your foreign code. These are all powerful mechanisms for providing ways to extend your application without needing to know the exact details of how this will be done.

In this example, we'll imagine a Zone component in the parent application that designates a rendering zone for any component you might choose to render in your third-party runtime plugin:

Sidebar.js
// Parent App somewhere
import React from 'react';
import { Zone, useHookComponent } from 'reactium-core/sdk';

const SideBar = props => {
  const SideBarHeader = useHookComponent('ZoneHeader');
  return (
    <section className='sidebar-zone'>
      <SideBarHeader {...props} />
      <Zone zone='sidebar' {...props} />
    </section>
  );
};

export default SideBar;

Above we have a hypothetical SideBar component in your parent application. There are two aspects of this component that can be extended dynamically by a plugin:

  1. A dynamic Zone, which can render one or more unknown components provided by your any plugin. Each component rendered by the Zone will receive any properties passed to the Zone component.

  2. A hook component, which may or may not exist (will be a null component by default), which may be implemented by some plugin.

In your runtime plugin directory, create a MenuItem.js file (doesn't matter what you call this). Implement a React component, and register it to the sidebar zone in your plugin:

src/app/components/plugin-src/my-runtime-feature/index.js (version 1)
import Reactium from 'reactium-core/sdk';
import MenuItem from './MenuItem';

// defer any code until plugins are mounted
Reactium.Plugin.register('my-plugin').then(() => {
  Reactium.Zone.addComponent({
    zone: ['sidebar'],
    component: MenuItem,
    order: Reactium.Enums.priority.lowest,
  });
});

Now in your parent application, when the SideBar component renders, your MenuItem component will dynamically render, even if your plugin is a 3rd party plugin, loaded into the browser.

In addition, you may wish to implement the hook component named SideBarHeader, create a component in your plugin in a file named SideBarHeader.js (again this name does not matter). Using the SDK, register this component:

src/app/components/plugin-src/my-runtime-feature/index.js (version 2)
import Reactium from 'reactium-core/sdk';
import MenuItem from './MenuItem';
import SideBarHeader from './SideBarHeader';

// defer any code until plugins are mounted
Reactium.Plugin.register('my-plugin').then(() => {
  Reactium.Zone.addComponent({
    zone: ['sidebar'],
    component: MenuItem,
    order: Reactium.Enums.priority.lowest,
  });
  Reactium.Component.register('SideBarHeader', SideBarHeader);
});

Whenever a component uses useHookComponent to get the SidebarHeader component, now your component will render (instead of the null component).

Components registered with Reactium.Component.register() are replaceable. The last plugin chronologically (and by priority) to register a component will replace whatever implementation was previously being used.

In addition to providing components for the parent application to use, you may use components provided elsewhere in your own plugin if the parent application or another plugin registered any components. This provides a way to share components across codebase without necessarily needed to have them in your codebase at build-time. When you're done developing your plugin eject it to the Actinium Build-time plugin you created above:

From Reactium root:
$ npx reactium plugin eject

You will be prompted to select the plugin and destination directory, and can save this location with a label for subsequent builds.

In your target Actinium plugin location, the css and js assets of your runtime plugin will be copied. For example: If your plugin is called my-runtime-features, your assets will found in a plugin-assets directory, containing my-runtime-features.js and my-runtime-features-plugin.css.

.
└── plugin-assets
    ├── my-runtime-features-plugin.css
    └── my-runtime-features.js

Up until now, you've been developing your runtime plugin, but practically you've been doing this at build-time. To see your plugin actually served at runtime, you need register these assets with the Actinium plugin. Register each asset using the Actinium Plugin SDK:

plugin.js
const path = require('path');
const PLUGIN = {
  ID: 'MyPlugin',
  name: 'My Plugin',
  description: 'My Actinium Plugin. Facilitates runtime plugin when activated.',
  version: {
    plugin: '1.0.0',
  },
};

Actinium.Plugin.register(PLUGIN);

Actinium.Plugin.addScript(path.resolve(
  PLUGIN.ID,
  __dirname, 'plugin-assets/my-runtime-features.js'
  ),
  'my-app' // app name, defaults to 'admin'
);

Actinium.Plugin.addStylesheet(path.resolve(
  PLUGIN.ID,
  __dirname, 'plugin-assets/my-runtime-features-plugin.css'
  ),
  'my-app' // app name, defauts to 'admin'
);

Now, when this plugin is installed and activated, these assets will be stored and served on the API. The API path URI to these assets will be added to the plugin metadata.

For Reactium Admin, active Actinium plugins that have registered assets of type 'admin' will automatically be served to your Admin at runtime. Additional work may be required in your target app, otherwise. See Serving Runtime Assets below.

Serving Runtime Assets

Now that you have configured your Actinium plugin to serve your runtime plugin assets, you will want your Reactium application to serve those assets. To do so, in some build-time plugin directory in your application (anywhere in your application src or any reactium_module), create a reactium-boot.js file, and load active plugins from the API, like so:

reactium-boot.js
// node/express code (es-modules allowed)
import _ from 'underscore';
const { Hook, Enums } = ReactiumBoot; // A global on the server 

// Get all the API plugins as a list from the server and assign to
// nodejs global.plugins
Hook.register('Server.AppGlobals', async (req, AppGlobals) => {
    try {
        const { plugins } = await Reactium.Cloud.run('plugins');

        // makes plugins configuration available on global for node.js and
        // window for your front-end code
        AppGlobals.register('plugins', {
            value: plugins,
            order: Enums.priority.highest,
        });
    } catch (error) {
        console.error('Unable to load plugins list', error);
    }
});

// for each active plugin with script targeting this app, register
// to be served to the browser
Hook.registerSync(
    'Server.AppScripts',
    (req, AppScripts) => {
        _.sortBy(op.get(global, 'plugins', []), 'order').forEach(plugin => {
            const script = op.get(plugin, 'meta.assets.my-app.script');
            AppScripts.unregister(plugin.ID);
            if (script && plugin.active) {
                const url = !/^http/.test(script) ? '/api' + script : script;
                AppScripts.register(plugin.ID, {
                    path: url,
                    order: Enums.priority.high,
                });
            }
        });
    },
    Enums.priority.highest,
);

// for each active plugin with stylesheet targeting this app, register
// to be served to the browser
Hook.registerSync(
    'Server.AppStyleSheets',
    (req, AppStyleSheets) => {
        _.sortBy(op.get(global, 'plugins', []), 'order').forEach(plugin => {
            const style = op.get(plugin, 'meta.assets.my-app.style');
            AppStyleSheets.unregister(plugin.ID);
            if (style && plugin.active) {
                const url = !/^http/.test(style) ? '/api' + style : style;
                AppStyleSheets.register(plugin.ID, {
                    path: url,
                    order: Enums.priority.high,
                });
            }
        });
    },
    Enums.priority.highest,
);

To test your static assets delivered from the browser through the API, toggle off your local development:

# from your Reactium application root
npx reactium plugin local

Select your runtime plugin to toggle off local development, and restart your node server.

Toggling local development on/off changes the development: true to false in your runtime plugins' reactium-hooks.json file. This can also be performed manually, or with the CLI.

After restarting the server, you should now see your runtime plugin .js and .css served on the page, and it should function, even if you comment out the index.js file in your plugin locally.

Styles and Assets

Runtime plugins present some unique challenges when it comes to assets you will need for your styles. By default (if you haven't extended your webpack configuration), style loaders and asset loaders are not included in your runtime plugin UMD (Universal Module Definition) build. Reactium's opinion, out-of-the-box, is to utilize SCSS (Sassy CSS) to pre-process styles for your application, and create CSS assets for your app. Even if you were to use webpack to load styles, for production they would often have to be extracted again anyway to avoid FOUC (Flash of unstyled content). This also slows down your Javascript build dramatically over time, and we prefer to process CSS separately.

For build-time plugins, this is not much of a problem, as your styles are usually incorporated into the larger stylesheet at build-time with an import statement.

For runtime plugins, this can mean coming up with some way to use styling for local development, and you will need to understand how that differs from using the runtime plugin css in the wild.

Assets

Reactium uses gulp tasks to copy and optimize assets it finds under any assets directory, and they often are served at build-time in a flattened /assets/ URI off the document root. This means that for build-time plugin, you can have CSS background images, and predict where they will be served both for local development and in production (e.g. /assets/images/my-background.jpg could be in your CSS as background: url('/assets/images/my-background.jpg')

For runtime plugins, running in production, these assets won't exist (or would exist on a completely different URL, so hard-coding this URL into CSS isn't gonna work), so it would be nice to use them in the stylesheet in a way that will work for both local development and production.

Reactium offers a supplementary DDD asset style-assets.json, which can allow the runtime-plugin developer to designate certain assets to be embedded in their stylesheet.

style-assets.json
{
  "background1": "images/background1.jpg"
}

Note: style-assets.json can be placed in any directory under src/app, and will gulp will produce a _reactium-style-variables.scss partial in that same directory. File paths found in the plugin-assets.json must be relative to this json file.

Now that I've created this file, given the existence of the relative files themselves, when I start the local development environment (or run the production build), these assets will be encoded into a SCSS partial in the same directory that can now be used in my runtime plugin's stylesheet. This partial will define an $assets variable which will be a SCSS map using the property names you specified in your plugin-assets.json, and a data url for the value.

.bg1 {
     // add the data-url for background1.jpg to my
     // compiled css
     background: url(map-get($assets, 'background1'));
}

In this way, it is possible to bundle your runtime plugin CSS assets for production.

Where are my styles coming from?

In local development, it can be important to understand how the CSS is being loading into the browser. When your local development is toggled on, (reactium-hooks.json development is set to true), starting the local build will load the locally built css for the runtime plugin into the browser automatically. Changes will be streamed to the browser in real-time using browser sync.

When your local development is toggled off, (reactium-hooks.json development is set to false), styles must be loaded from the production CSS using some other mechanism in your app.

For the @atomic-reactor/admin plugin, any admin plugins registered in Actinium with CSS assets are already setup to be loaded via the API. You can setup a similar mechanism in your own application, see Serving Runtime Assets.

Separate Development / Production Codebase

It is important to know that you should not maintain your runtime plugin src in the same codebase as your deployed application, because its presence in the src/app/component/plugin-src in your deployable application would essentially negate the whole point of runtime plugins (i.e. code that is introduced to your app only at runtime.) Instead, you'll want to develop your runtime plugin in a separate copy of your base application, so that the compiled runtime assets can be served elsewhere (such as from a CDN or your API plugin). This is because when developing your runtime plugin, during this activity it is essentially a specially structured build-time plugin, but during normal deployed use, these assets are precompiled and loaded in the browser instead.

  • development codebase - consists of your base application plus one or more runtime plugin source directories under src/app/components/plugin-src These constitute the local development environment (uses same facilities as build-time plugins). A special umd.js DDD artifact provides the build entry point for creating the compiled runtime js asset.

  • application codebase - code here loads only the static assets compiled in development codebase, but the source of the runtime plugin cannot be found.

Note that both the reactium-hooks.js file and the umd.js file import from index.js in the boilerplate code. This is so you can make your changes in one place for both local dev and the production asset build, index.js.

Because in the target application codebase, you will only have browser-loaded js and CSS, some artifacts you would ordinary expect to work for a runtime plugin are not available in the runtime context.

A common mistake in creating runtime plugins is forgetting that you can't import components and code from the surrounding application using direct relative or webpack contextual imports (e.g. import Something from 'component/Something'; will work great in local development, but will break in the application codebase.) Only framework provided externals can be imported into your index.js entry point. See Create a Run-time plugin for list of externals that can be imported. You MAY import from NPM dev dependency libraries that are not listed, but note that these dependencies will be bundled with your runtime library, sometimes considerably adding to the duplicate weight of the plugin. Consider "hosting" these components using the parent application (e.g. Reactium.Component.register()to make these available to all the runtime plugins vis useHookComponent())

Treat your plugin src directory as something that should be encapsulated or interacting only with the SDK, not as part of the larger application codebase, or you risk it not building correctly for runtime usage. Build-time DDD artifacts like route.js, reactium-hooks.js, reactium-boot.js, state.js, actions.js, reducers.js, etc. will work in the local development environment, but will not be included in your UMD, and will not apply in the application codebase. Hooks registrations, SDK extensions, component registrations, should be added to the index.js entry point, and reactium-hooks.js should not be modified in your local development codebase, as it will create confusing differences between dev and production.

Publish A Run-time Plugin

Once your Reactium Run-time Plugin has been ejected into your Actinium Build-time Plugin you can publish the Actinium Build-time Plugin to the Reactium Plugin Registry:

From Actinium build-time plugin directory:
npm reactium publish

Install A Run-time Plugin

Once the Actinium Build-time Plugin serving the Reactium Run-time Plugin has been published, you can install it to any Actinium project which will make it available in your Reactium project:

From Actinium root:
npx reactium install @myscope/MyPlugin

Last updated