npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

code-plug

v0.6.1

Published

Plugin architecture

Downloads

15

Readme

CodePlug is not a UI component library and is not a framework, it's just a way to organize the code in order to fight the monolith.

What's a monolith? Every long term project has the same enemy: the complexity. Soon or later, feature after feature, the code base slowly tends towards a unique conglomerate of code: components and features become tightly coupled, old features are hard to remove, dead code is everywhere and introducing new features it's complex and bug prone.

Have you ever hesitated to delete old graphic assets from Christmas 2010 since your afraid that something might still use it? Do you use CSS class names to find the right feature in your code base? If the answer is yes, then CodePlug is for you.

How do you fight the monolith? With the plugin-first method.The plugin-first method has 3 simple rules

  1. Every feature is a plugin
  2. All files related to a plugin are packaged in the same folder
  3. If you remove the plugin, the application must not break

With the plugin-first method you get:

  • Removing dead code it's easy: if the feature is gone, then we can remove safely the whole directory and - thanks to rule #3 - your application will not break
  • No need to "hide" features based on user permissions, just don't load the plugin (and the app will not break)
  • No need to disable plugin based on the environment, just don't load the plugin in that environment (and the app will not break)
  • Do you need to expose some features to a restricted set of beta users? Just load the plugin for these users
  • Multiple version of the same app with different set of features ... you know the answer

How does it work? It looks like a mediator pattern: each plugin registers one or more React views under a "region". A simple helper collects all the registered views for a "region" and renders them. Here is the trick: the features definition is totally decoupled from its instantiation and that makes your code more robust, if the plugin is not loaded the feature it's simply not there: nothing can break the app if it's not present.

Let's make an example with a very simple button bar:

import { Plugin, CodePlug, PluginViews } from 'code-plug';
import { Button } from 'my-ui-lib';
// this is the red button feature
class RedButton extends Plugin {
  constructor(props) {
    super(props);
    this.register('my_buttons', Button, {
      id: 'red',
      label: 'Red',
      style: { background-color: '#FF0000' } 
    });
  }
}
// this is the blue button feature
class BlueButton extends Plugin {
  constructor(props) {
    super(props);
    this.register('my_buttons', Button, {
      id: 'blue',
      label: 'Blue',
      style: { background-color: '#0000FF' } 
    });
  }
}
// the main app
class App extends React.Component {
  render() {
    return (
      <CodePlug plugins={[RedButton, BlueButton]}>
        <div className="my_app">
          <div className="buttons">
            <PluginViews 
              region="my_buttons" 
              onClick={() => alert('Clicked!')}
            />
          </div>
          <div className="main">
            ...
          </div>
        </div> 
      </CodePlug>
    );
  }
}

Consider that:

  • Plugins can register any number of views for any region name, could be a simple button or an entire page
  • Every parameter used in the registration will be passed as props to the registered views on rendering
  • In order to manage which features/plugins are present in the app, just pass an array of plugin-class in the prop plugins: how you do it (based on permissions, environment, etc) is up to you and it's out of the scope of CodePlug
  • PluginViews it's just an helper that collect views for a named region and render them, every props (except "region") will be passed to the rendered views

You also get long term advantages with this approach: think again at the example of the buttons, months after months of coding things with these buttons are going to get complicated, they can be visible or not - or just disabled - for various reasons (domain logic, permission logic, environment logic). With the plugin-first method the why is defined in the plugin at the highest level of your code (for example a button is disabled since the user has not the permission) and the how is defined in the views at the lowest level of the code (for example a button looks disabled with a simple CSS rule). You are never going to mix these two, the behaviour of the buttons will always be very easy to inspect ad the top level of your code and well separated by the implementation detail of the button (for example how it's rendered when disabled).

Let's take the example above and complicate it a little: now the buttons should be present only if the user has the related permission and must be disabled based on some domain logic (for example you cannot archive a blog post which is already archived)

import { Plugin, CodePlug, PluginViews, FilterByPermission } from 'code-plug';
import { Button } from 'my-ui-lib';
// this is the archive button feature
class ArchiveButton extends Plugin {
  static permission = 'canArchive';
  constructor(props) {
    super(props);
    this.register('my_buttons', Button, ({ blogPost }) => ({
      id: 'archive',
      label: 'Archive',
      disabled: blogPost.archived,
      onClick: () => { // archive ... }
    }));
  }
}
// this is the delete button feature
class DeleteButton extends Plugin {
  static permission = 'canDelete';
  constructor(props) {
    super(props);
    this.register('my_buttons', Button, ({ blogPost }) => ({
      id: 'delete',
      label: 'Delete',
      disabled: blogPost.deleted,
      onClick: () => { // delete ...} 
    }));
  }
}
// the main app
class App extends React.Component {
  const { currentUser } = this.props;
  render() {
    return (
      <CodePlug 
        plugins={[FilterByPermission, ArchiveButton, DeleteButton]}
        user={currentUser} // has a permissions[] field
      >
        <div className="my_app">
          <div className="buttons">
            <PluginViews 
              region="my_buttons" 
              blogPost={myBlogPost} 
            />
          </div>
          <div className="main">
            // ... tha main app
          </div>
        </div> 
      </CodePlug>
    );
  }
}

Consider that:

  • FilterByPermission is an hook: it skip the loading of a plugin based on permissions, it assumes the currentUser object has a permissions props (array of string). In order to be loaded the static prop permission of the plugins must be included in permissions. There's a bunch of these hooks packed with CodePlug, but you can easily write your own.
  • In this case the registered views props depend also on some domain logic available only at runtime (cannot archive and archived blog post) and cannot be coded statically into a plugin definition. For this reason the props of a registered view can be defined as a function that takes as parameter the props passed to <PluginViews>
  • If currentUser.permissions doesn't include the right permission string (canDelete or canArchive) the related plugin is simply not loaded and the feature will not be present
  • What if you need an UI element present in another plugin? This is a component, something that you can re-use through the app and should be placed elsewhere (for example /ui/*), a plugin - by definition - is something that you can remove from the app without breaking it

Render Prop

The problem of the above example can be also resolved with a render prop in <PluginViews>:

render() {
  const { blogPost, currentUser } = this.props;
  return (
    <div className="buttons">
      <PluginViews region="my_buttons" />
        (View, props, plugin) => (
          <View {...props} disabled={currentUser.permissions.includes(plugin.permission)}/>
        )
      </PluginViews>
    </div>
  );
}

To the render props are passed these arguments: the registered React view, the registered props, the instance of the plugin (where the view was registered).

Key Prop

The <PluginViews> returns an array of React views, each view must have a distinct key prop (as per React requirement). CodePlug is able to resolve this prop using the static properties of the registered view (the component name or the displayName property).

There is a corner case in which the plugin register the same class view with different props (the <button> in the example above), in this case just include an id key in the registered prop (id: 'archive') to generate different keys for the same component class.

Items helper

Sometimes it's needed to get items for a particular region without a render

render() {
  return (
    <Items region="my_region" my_prop="42">
      {items => items.map(({ view, props, plugin}) => {
          // do something useful
          // view - the registered view
          // props - merge of registered props and Items prop (includes my_prop = 42)
          // plugin - the instance of the plugin
        });
      }}
    </Items>
  );
}

the same helper is available as method in CodePlug, the code above could be also written


render() {
  return (
    <CodePlug 
      ref={ref => this.codePlug = ref}
      plugins={[FilterByPermission, ArchiveButton, DeleteButton]}
      user={currentUser} // has a permissions[] field
    >
      {this.codePlug
        .getItems('my_region', { my_prop: 42})
        .map(({ view, props, plugin}) => {
          // do something useful
          // view - the registered view
          // props - merge of registered props and Items prop (includes my_prop = 42)
          // plugin - the instance of the plugin
        })
      }
      // do something useful
    </CodePlug>
  );
}

Hooks

A hook is special plugin that filter which plugin is actually instantiated in CodePlug. For example to load only the set of plugins the current user has access to:

function(plugin) {
  const { user, debug } = this.props || {};
  const permission = plugin != null && plugin.prototype != null && plugin.prototype.constructor != null ?
    plugin.prototype.constructor.permission : null;
  if (user != null && user.permissions != null && user.permissions.includes(permission)) {
    return true;
  }
  if (debug) {
    console.log(`Plugin ${plugin != null ? plugin.name : 'unnamed'} not loaded, missing ${permission}!`);
  }
}
  • Every plugin has a permission property
  • The object user passed to <CodePlug> has a property permissions (array of strings)
  • If the function returns a truthy value, then the plugin is instantiated, otherwise is discarded

API Reference

Plugin Class

| Method | Params | Description | | --- | --- | --- | | register(region[, view][, props]) | plugin | Register a view and or a set of properties for a region (mandatory) |

CodePlug

Properties:

| Property | Params | Description | | --- | --- | --- | | plugins | [Plugin] | List of plugin classes to be instantiated | | debug | Boolean | Show some debug information about the behaviour of hooks |

Methods

| Method | Params | Description | | --- | --- | --- | | getItems(region[, props]) | [Item] | Get all items for a region, return an array of items | | getPlugins | [plugin] | List of installed plugins | | getAllPlugins | [Plugin] | List of all plugin classes |

Views

| Property | Params | Description | | --- | --- | --- | | region | String / [String] | Render all views associated with the region(s) |

Items

| Property | Params | Description | | --- | --- | --- | | region | String / [String] | Get all items associated with the region(s) |

Item

| Value | Params | Description | | --- | --- | --- | | view | React | The React view | | props | Object | The evaluated props associated with the view, it's the merge of the props registered statically or dinamically with the .register() method the the ones passed by the Views, Items tag or .getItems() method | | plugin | Plugin | The plugin instance that registered the view |

Examples

Mqtt Client is a small MQTT desktop client, its purpose is to listen for multiple MQTT topics and trigger some actions on a desktop computer and plot data on a map. It's based on Electron and has a plugin-first architecture: every rules and action that can be applied to an incoming message is defined in plugins that can be added, removed, etc.