Writing a no-build plugin

This guide covers creating a no-build plugin for JBrowse 2.

Pre-requisites

  • you can run an instance of JBrowse 2 on the web, see any of our quickstart guides for details
  • a stable and recent version of node
  • basic familiarity with the command line and navigating the file system

What is the difference between a no-build plugin and a "regular" plugin?

A "regular" JBrowse plugin uses the plugin template, which uses rollup to bundle extra dependencies.

In contrast, "no-build" plugins have no build step and can be hand edited. This can be useful for adding extra jexl config callbacks for making extra config callbacks or similar modifications.

Writing a "no-build" plugin

Example use case: Adding a jexl callback function which you can use in your config

A common pattern is registering a custom jexl function to simplify config callbacks. Create myplugin.js:

myplugin.js

export default class MyPlugin {
  name = 'MyPlugin'
  version = '1.0'

  install(pluginManager) {
    pluginManager.jexl.addFunction('customColor', feature => {
      if (feature.get('type') === 'exon') {
        return 'red'
      } else if (feature.get('type') === 'CDS') {
        return 'green'
      }
    })
  }

  configure(pluginManager) {}
}

Put myplugin.js alongside your config file and reference it in config.json:

{
  "plugins": [
    {
      "name": "MyPlugin",
      "esmLoc": {
        "uri": "myplugin.js"
      }
    }
  ],
  "tracks": []
}

Example use case: Adding a global menu item

Another example of a no-build plugin is to add menu items or minor extension points. Here, we're going to add a menu item using the configure method in the plugin class.

myplugin.js

export default class MyPlugin {
  name = 'MyPlugin'
  version = '1.0'
  install() {}
  configure(pluginManager) {
    // this is called in the web worker as well, which does not have a
    // rootModel, so check for existence of pluginManager.rootModel before
    // continuing
    if (pluginManager.rootModel) {
      // adding a new menu to the top toolbar
      pluginManager.rootModel.insertMenu('Citations', 4)

      // appending a menu item to the new menu
      pluginManager.rootModel.appendToMenu('Citations', {
        label: 'Cite this JBrowse session',
        onClick: session => {
          /* do nothing for now, see below for example */
        },
      })
    }
  }
}

Importing with jbrequire

Since no-build plugins have no build step, use jbrequire to access packages exported by JBrowse core. See the full list.

const { types } = pluginManager.jbrequire('@jbrowse/mobx-state-tree')

Complete example

esmplugin.js

export default class MyPlugin {
  name = 'MyPlugin'
  version = '1.0'

  install(pluginManager) {
    const { ConfigurationSchema } = pluginManager.jbrequire(
      '@jbrowse/core/configuration',
    )
    const WidgetType = pluginManager.jbrequire(
      '@jbrowse/core/pluggableElementTypes/WidgetType',
    )
    const { ElementId } = pluginManager.jbrequire(
      '@jbrowse/core/util/types/mst',
    )
    const { types } = pluginManager.jbrequire('@jbrowse/mobx-state-tree')

    const React = pluginManager.jbrequire('react')

    const CiteWidget = props => {
      // React.createElement can be used to add html to our widget component.
      // We write out raw React.createElement code because JSX requires a build
      // step and can't be used very easily in the no build plugin context
      const header = React.createElement(
        'h1',
        null,
        'Cite this JBrowse session',
      )
      const content = React.createElement(
        'p',
        null,
        'Diesh, Colin, et al. "JBrowse 2: A modular genome browser with views of synteny and structural variation." bioRxiv. 2022.',
      )

      return React.createElement('div', null, [header, content])
    }

    pluginManager.addWidgetType(() => {
      return new WidgetType({
        name: 'CiteWidget',
        heading: 'Cite this JBrowse session',
        configSchema: ConfigurationSchema('CiteWidget', {}),
        stateModel: types.model('CiteWidget', {
          id: ElementId,
          type: types.literal('CiteWidget'),
        }),
        ReactComponent: CiteWidget,
      })
    })
  }

  configure(pluginManager) {
    if (pluginManager.rootModel) {
      pluginManager.rootModel.insertMenu('Citations', 4)

      pluginManager.rootModel.appendToMenu('Citations', {
        label: 'Cite this JBrowse session',
        onClick: session => {
          // upon clicking on this menu item, we need to add and show our new widget
          const widget = session.addWidget('CiteWidget', 'citeWidget', {})
          session.showWidget(widget)
        },
      })
    }
  }
}

Then in your config you can reference it using the "esmLoc" function

{
  "plugins": [
    {
      "name": "MyPlugin",
      "esmLoc": {
        "uri": "esmplugin.js"
      }
    }
  ],
  "tracks": []
}

Result

With JBrowse running and the "Citation" plugin from above added to your config, your JBrowse session should look like the following:

Screenshot of a running JBrowse instance with the simple no build plugin added. Note our top level menu item has been added, and upon clicking it our widget opens.
Screenshot of a running JBrowse instance with the simple no build plugin added. Note our top level menu item has been added, and upon clicking it our widget opens.

Next steps

Have questions? Contact us.

Note: JSX syntax

Writing React code without JSX is more verbose since JSX requires a build step. If your plugin has dependencies or you prefer TypeScript, use the plugin template which includes a build step, bundler, and type checking.

Note: UMD vs ESM module syntax

This guide uses ESM modules (exporting a plain class), which all modern browsers support. For legacy browser compatibility you can also use UMD modules — see this example, which defines a specific global variable rather than exporting a class.

Note: Plugins in embedded React components

This no-build guide targets jbrowse-web, which loads plugins via plugins.json. If you are using an embedded component (@jbrowse/react-app2 or @jbrowse/react-linear-genome-view2) the approach is different: define your plugin as a class and pass it in the plugins array to createViewState:

import { createViewState, JBrowseApp } from '@jbrowse/react-app2'

class MyPlugin {
  name = 'MyPlugin'
  install(pluginManager) {
    /* ... */
  }
  configure() {}
}

const state = createViewState({ config, plugins: [MyPlugin] })

See the Using Plugins story in the @jbrowse/react-app2 storybook for a live example.