Skip to main content

Plugins

How a plugin presents itself

name is an identifier: @app/notebook is what other plugins depend on and what a dependency graph is drawn from. It is not what a person should be shown.

Four optional fields say that instead — and the same four exist on the Python PluginManifest, so a host listing both tiers never has to ask which side a plugin came from.

definePlugin({
name: '@app/notebook',
displayName: 'Notebook',
description: 'Runs notebooks, and owns the kernel they run on.',
octicon: 'book', // an id, not a component
emoji: '📓',
});

The icon is named rather than imported on purpose. A component could only come from the tier that imported it; an id is a string a Python manifest can carry too, and the host decides what it draws — which is the only place that decision belongs.

Read it back with getManifest, or in React with usePluginManifests():

reactor.getManifest('@app/notebook');
// { name, displayName: 'Notebook', description, octicon, emoji, version,
// requiredBackendPlugins: [], optionalBackendPlugins: [], lazy, loaded }

Nothing here is required. A plugin that says none of it still runs, and displayName falls back to name, so a host always has something to print.

Declaring what a plugin needs

definePlugin({
name: '@app/notebook',
dependencies: [BasePlugin], // must be present, and activated first
peerDependencies: [ThemePlugin], // used if present
conflictsWith: ['@app/legacy-notebook'],
});

Dependencies decide activation order and are what deactivation walks when it stands dependants down.

For the other wire — what this plugin needs from the Python tier — see Cross-tier Dependencies.

What a plugin contributes

Two ways, and they are not interchangeable:

export const NotebookPlugin = definePlugin({
name: '@app/notebook',
// Declarative: resolved during the register phase.
contributes: [
contribution(
ViewTypePoint,
{ title: 'Notebook', load: () => import('./NotebookView') },
{ id: 'notebook', order: 10 },
),
],
// Imperative: for contributions that depend on build output, or that appear
// later. Returns a disposer.
register(ctx) {
if (ctx.reactor.hasPlugin('@app/sandbox')) {
ctx.contribute(ViewTypePoint, { title: 'Sandbox', load: () => import('./SandboxView') }, { id: 'sandbox' });
}
},
});

Declare with contributes when nothing about the contribution depends on what the plugin built. Reach for ctx.contribute when it does — or when the contribution is not knowable until something else has registered.

Slot components

A plugin's build() may return components for named slots. Everything contributed to a slot is rendered; see slots or contribution points.

definePlugin({
name: '@app/header',
build() {
return {
components: [
{ slot: 'header', id: 'app-header', Component: AppHeader },
],
};
},
});

Keep the entry point light

The rule that pays for everything on the lazy loading page: anything a plugin's index imports lands in the shell's bundle, however lazy the plugin's views are. A plugin whose manifest file imports a charting library has already spent the money.

Put the manifest in index.ts, and the code in a module it loads.