Writing a no-build plugin
TL;DR: a no-build plugin is a single hand-edited .js file next to your
config.json, referenced from the plugins array with esmLoc. It needs no
bundler and no npm install, which makes it the right shape for
jexl config callbacks or similar modifications
— at the cost of no JSX, no TypeScript, and no dependencies beyond what JBrowse
re-exports.
A "regular" plugin uses the
plugin template and bundles
dependencies with rollup. The only prerequisite here is a running JBrowse 2
instance to load the file into (see
any of our quickstart guides).
Adding a jexl callback
Register a custom jexl function from your plugin's install(), to simplify
config callbacks:
// a jexl function usable from any config callback, e.g.
// "jexl:customColor(feature)" as a track's color1
pluginManager.jexl.addFunction('customColor', feature => {
if (feature.get('type') === 'exon') {
return 'red'
} else if (feature.get('type') === 'CDS') {
return 'green'
}
return 'goldenrod'
})
Put myplugin.js alongside your config file and reference it in config.json:
{
"plugins": [
{
"name": "MyPlugin",
"esmLoc": {
"uri": "myplugin.js"
}
}
],
"tracks": []
}
Adding a global menu item
This adds a menu item via the plugin's configure method:
configure(pluginManager) {
// configure runs in the web worker too, and there is no rootModel there —
// so guard on it before touching any menu
if (pluginManager.rootModel) {
// a new menu in the top toolbar, at index 4
pluginManager.rootModel.insertMenu('Citations', 4)
pluginManager.rootModel.appendToMenu('Citations', {
label: 'Cite this JBrowse session',
onClick: session => {
const widget = session.addWidget('CiteWidget', 'citeWidget', {})
session.showWidget(widget)
},
})
}
}
Importing with jbrequire
With no build step, reach the shared libraries JBrowse re-exports (React, MobX,
MST, MUI, @jbrowse/core APIs) through jbrequire — e.g.
const { types } = pluginManager.jbrequire('@jbrowse/mobx-state-tree'). The
complete example below uses it five times. See
Dependencies and re-exports and the
canonical list.
Complete example
esmplugin.js
export default class MyPlugin {
name = 'MyPlugin'
version = '1.0'
install(pluginManager) {
// a jexl function usable from any config callback, e.g.
// "jexl:customColor(feature)" as a track's color1
pluginManager.jexl.addFunction('customColor', feature => {
if (feature.get('type') === 'exon') {
return 'red'
} else if (feature.get('type') === 'CDS') {
return 'green'
}
return 'goldenrod'
})
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 = () => {
// React.createElement rather than JSX: JSX needs a build step, which is
// the one thing a no-build plugin does not have.
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." Genome Biology 24, 74 (2023).',
)
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) {
// configure runs in the web worker too, and there is no rootModel there —
// so guard on it before touching any menu
if (pluginManager.rootModel) {
// a new menu in the top toolbar, at index 4
pluginManager.rootModel.insertMenu('Citations', 4)
pluginManager.rootModel.appendToMenu('Citations', {
label: 'Cite this JBrowse session',
onClick: session => {
const widget = session.addWidget('CiteWidget', 'citeWidget', {})
session.showWidget(widget)
},
})
}
}
}
Reference it from your config with esmLoc:
{
"plugins": [
{
"name": "MyPlugin",
"esmLoc": {
"uri": "esmplugin.js"
}
}
],
"tracks": []
}
The finished plugin
With the "Citation" plugin above in your config, the session looks like this:
Note: JSX syntax
React without JSX is more verbose since JSX needs a build step. If your plugin has dependencies or you prefer TypeScript, use the plugin template, which includes a bundler and type checking.
Note: UMD vs ESM module syntax
This guide uses ESM modules (a plain exported class), supported by all modern browsers. For legacy browsers you can use UMD modules instead. See this example, which defines a global variable rather than exporting a class.
Note: Plugins in embedded React components
This guide targets jbrowse-web, which loads plugins via config.json. Embedded
components (@jbrowse/react-app2 or @jbrowse/react-linear-genome-view2) have
no config.json, so they take the class directly — declared in the same file, and
passed in plugins:
export default function EmbeddedPlugin() {
return (
<JBrowse
assemblies={assemblies}
tracks={tracks}
// the class itself, not a definition to fetch — an embedded app has no
// config.json to list plugins in
plugins={[HighlightRegionPlugin]}
views={[
{
type: 'LinearGenomeView',
init: {
assembly: 'volvox',
loc: 'ctgA:1..50000',
tracks: ['volvox_cram'],
},
},
]}
/>
)
}
createViewState({ config, plugins: [MyPlugin] }) takes the same array. To
fetch a published plugin at runtime instead, loadPlugins returns records to
pass through unchanged — see
With external plugin.