Skip to content

PluginManager

import * as PluginManager from '@dxos/app-framework/PluginManager';
import * as UrlLoader from '@dxos/app-framework/UrlLoader';

The plugin manager holds an app’s plugins, decides which are enabled, and activates their modules as events fire. An app such as Composer creates one at boot; a plugin reaches it through Capabilities.PluginManager when it needs to turn other plugins on or off. Activation explains the order it activates modules in.

PluginManager.make takes the plugins the app knows about and the ones enabled at start. Plugins tagged system are always enabled. The pluginLoader turns an id the manager is later asked to add into a plugin.

import * as
import PluginManager
PluginManager
from '@dxos/app-framework/PluginManager';
import * as
import UrlLoader
UrlLoader
from '@dxos/app-framework/UrlLoader';
import {
const make: PluginFactory<void>
make
} from './dx.config.ts';
const
const stash: Plugin
stash
=
function make(options: void): Plugin
make
();
export const
const manager: PluginManager.PluginManager
manager
=
import PluginManager
PluginManager
.
const make: (options: PluginManager.ManagerOptions) => PluginManager.PluginManager

Creates a new Plugin Manager instance.

make
({
plugins?: Plugin[] | undefined
plugins
: [
const stash: Plugin
stash
],
enabled?: string[] | undefined
enabled
: [
const stash: Plugin
stash
.
Plugin.meta: Readonly<{
readonly profile: {
readonly key: string;
readonly name: string;
readonly description?: string | undefined;
readonly homePage?: string | undefined;
readonly source?: string | undefined;
readonly tags?: readonly string[] | undefined;
readonly screenshots?: readonly {
readonly light?: string | undefined;
readonly dark?: string | undefined;
}[] | undefined;
readonly icon?: {
readonly key: string;
readonly hue?: string | undefined;
} | undefined;
readonly dependsOn?: readonly string[] | undefined;
readonly spec?: string | undefined;
readonly createdAt?: string | undefined;
readonly author?: string | undefined;
};
readonly release?: {
...;
} | undefined;
}>
meta
.
profile: {
readonly key: string;
readonly name: string;
readonly description?: string | undefined;
readonly homePage?: string | undefined;
readonly source?: string | undefined;
readonly tags?: readonly string[] | undefined;
readonly screenshots?: readonly {
readonly light?: string | undefined;
readonly dark?: string | undefined;
}[] | undefined;
readonly icon?: {
readonly key: string;
readonly hue?: string | undefined;
} | undefined;
readonly dependsOn?: readonly string[] | undefined;
readonly spec?: string | undefined;
readonly createdAt?: string | undefined;
readonly author?: string | undefined;
}
profile
.
key: string
key
],
// Turns an id `add` is given into a plugin; this one loads manifest URLs.
pluginLoader: (id: string) => Effect.Effect<PluginManager.LoadedPlugin, Error>
pluginLoader
:
import UrlLoader
UrlLoader
.
const make: (builtinPlugins: Plugin[], options?: UrlLoader.Options) => (locator: string) => Effect.Effect<{
plugin: Plugin;
dev?: boolean;
}, UrlLoader.RemotePluginLoadError>

Creates a plugin loader that resolves built-in plugins by ID or loads remote plugins from URLs.

Remote URLs must point at a plugin manifest (plugin.json). The loader fetches the manifest, eagerly persists every declared asset via the configured PluginAssetCache, then dynamic-imports the entry module.

make
([]),
});

start runs the startup pass: it activates the Startup modules and every module whose requirements they satisfy. An app fires Idle once it has painted. A React app renders App.Root from @dxos/app-framework/App, which does both.

import * as
import Effect
Effect
from 'effect/Effect';
import * as
import ActivationEvents
ActivationEvents
from '@dxos/app-framework/ActivationEvents';
export const
const start: Effect.Effect<void, Error, never>
start
=
import Effect
Effect
.
const gen: <Effect.Effect<boolean, Error, never>, void>(f: () => Generator<Effect.Effect<boolean, Error, never>, void, never>) => Effect.Effect<void, Error, never> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to use

Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.

Generator functions work similarly to async/await but keep errors, requirements, and interruption in the Effect type. You can yield* values from effects and return the final result at the end.

Example (Sequencing effects with generators)

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})
await Effect.runPromise(program) // => "Final amount to charge: 96"

@stability ― stable

@category ― constructors

@since ― 2.0.0

gen
(function* () {
yield*
const manager: PluginManager.PluginManager
manager
.
PluginManager.start(): Effect.Effect<boolean, Error>

Runs startup: the capability-dependency resolution pass for dependency-mode modules, concurrently with the event-mode activation pass for any module explicitly targeting the Startup event. The event-level Startup activated message publishes only after both passes complete. Idempotent — subsequent calls activate whatever registered since. activate(Startup) delegates here.

start
();
yield*
const manager: PluginManager.PluginManager
manager
.
PluginManager.activate(event: ActivationEvent | string, params?: {
before?: string;
after?: string;
}): Effect.Effect<boolean, Error>
activate
(
import ActivationEvents
ActivationEvents
.
const Idle: ActivationEvent

Fired once by the host at main-thread idle after the app is interactive.

For REGISTRATION contributions only — graph builders, operation handler sets, settings: the app-wide indexes a user can reach from anywhere (the navtree, the operation registry, a settings read) and which therefore no single surface can gate. A feature's own modules ride its plugin start event, fired when its surface renders; putting a feature body here instead is what turned the former DeferredStartup into a second startup pass.

This is also what omitting activatesOn normalizes to, so a module that must run at boot has to say activatesOn: ActivationEvents.Startup explicitly. Defined in core; see

ActivationEvent$.Idle

.

Idle
);
});

enable turns on a plugin and the plugins it declares in dependsOn, then activates every module whose event has fired. disable deactivates a plugin’s modules and, by default, the plugins that depend on it. remove disables a plugin and forgets it.

import * as
import Effect
Effect
from 'effect/Effect';
export const
const toggle: (key: string) => Effect.Effect<void, Error, never>
toggle
= (
key: string
key
: string) =>
import Effect
Effect
.
const gen: <Effect.Effect<boolean, Error, never>, void>(f: () => Generator<Effect.Effect<boolean, Error, never>, void, never>) => Effect.Effect<void, Error, never> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to use

Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.

Generator functions work similarly to async/await but keep errors, requirements, and interruption in the Effect type. You can yield* values from effects and return the final result at the end.

Example (Sequencing effects with generators)

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})
await Effect.runPromise(program) // => "Final amount to charge: 96"

@stability ― stable

@category ― constructors

@since ― 2.0.0

gen
(function* () {
if (
const manager: PluginManager.PluginManager
manager
.
PluginManager.getEnabled(): readonly string[]
getEnabled
().
ReadonlyArray<string>.includes(searchElement: string, fromIndex?: number): boolean

Determines whether an array includes a certain element, returning true or false as appropriate.

@param ― searchElement The element to search for.

@param ― fromIndex The position in this array at which to begin searching for searchElement.

includes
(
key: string
key
)) {
// Also disables the plugins that depend on it.
yield*
const manager: PluginManager.PluginManager
manager
.
PluginManager.disable(id: string, opts?: {
cascade?: boolean;
}): Effect.Effect<boolean, Error>

Disables a plugin.

By default, cascades to currently-enabled dependents (transitively, leaves first) so disabling a depended-upon plugin never leaves its dependents stranded. Pass cascade: false to disable only the named plugin and leave its dependents enabled-but-broken — VS Code-style disable parity for callers that want the escape hatch (e.g. when swapping in an alternative implementation that satisfies the dependents' needs in its own way).

Fails with

Plugin.PluginDependencyError

(reason: 'core-dependent') when cascading would require disabling a core plugin; UI flows should surface their own confirmation before calling disable with the default.

disable
(
key: string
key
);
} else {
yield*
const manager: PluginManager.PluginManager
manager
.
PluginManager.enable(id: string, opts?: {
resolveDependencies?: boolean;
}): Effect.Effect<boolean, Error>

Enables a plugin.

Default behavior auto-resolves the plugin's declared dependsOn closure: missing entries that exist in the plugin registry catalog are installed via

add

, then enabled in dependency-first order. Set resolveDependencies to false to enable only the named plugin and skip the closure walk entirely — useful when substituting an alternative plugin that satisfies the dependent's capability needs in its own way.

enable
(
key: string
key
);
}
});

add hands an id to the plugin loader and registers the result without enabling it. UrlLoader.make loads a plugin from its manifest URL, which is how Composer’s Load dev plugin and the docs’ inspector load a plugin from its dev server. The page needs an import map of the shared packages, so the plugin uses the page’s copies.

import * as
import Effect
Effect
from 'effect/Effect';
export const
const loadDevPlugin: Effect.Effect<void, Error, never>
loadDevPlugin
=
import Effect
Effect
.
const gen: <Effect.Effect<Plugin, Error, never> | Effect.Effect<boolean, Error, never>, void>(f: () => Generator<Effect.Effect<Plugin, Error, never> | Effect.Effect<boolean, Error, never>, void, never>) => Effect.Effect<void, Error, never> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to use

Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.

Generator functions work similarly to async/await but keep errors, requirements, and interruption in the Effect type. You can yield* values from effects and return the final result at the end.

Example (Sequencing effects with generators)

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})
await Effect.runPromise(program) // => "Final amount to charge: 96"

@stability ― stable

@category ― constructors

@since ― 2.0.0

gen
(function* () {
// The plugin's own key may differ from the URL it was loaded from.
const
const plugin: Plugin
plugin
= yield*
const manager: PluginManager.PluginManager
manager
.
PluginManager.add(id: string): Effect.Effect<Plugin, Error>

Loads a plugin via the plugin loader and registers it without enabling it. Returns the loaded plugin so callers can enable it by its canonical id (which may differ from the locator used to load it, e.g. URL loaders). A plugin whose id is already enabled, such as one persisted from an earlier session, is activated as part of the add.

add
('http://localhost:3967/manifest.json');
yield*
const manager: PluginManager.PluginManager
manager
.
PluginManager.enable(id: string, opts?: {
resolveDependencies?: boolean;
}): Effect.Effect<boolean, Error>

Enables a plugin.

Default behavior auto-resolves the plugin's declared dependsOn closure: missing entries that exist in the plugin registry catalog are installed via

add

, then enabled in dependency-first order. Set resolveDependencies to false to enable only the named plugin and skip the closure walk entirely — useful when substituting an alternative plugin that satisfies the dependent's capability needs in its own way.

enable
(
const plugin: Plugin
plugin
.
Plugin.meta: Readonly<{
readonly profile: {
readonly key: string;
readonly name: string;
readonly description?: string | undefined;
readonly homePage?: string | undefined;
readonly source?: string | undefined;
readonly tags?: readonly string[] | undefined;
readonly screenshots?: readonly {
readonly light?: string | undefined;
readonly dark?: string | undefined;
}[] | undefined;
readonly icon?: {
readonly key: string;
readonly hue?: string | undefined;
} | undefined;
readonly dependsOn?: readonly string[] | undefined;
readonly spec?: string | undefined;
readonly createdAt?: string | undefined;
readonly author?: string | undefined;
};
readonly release?: {
...;
} | undefined;
}>
meta
.
profile: {
readonly key: string;
readonly name: string;
readonly description?: string | undefined;
readonly homePage?: string | undefined;
readonly source?: string | undefined;
readonly tags?: readonly string[] | undefined;
readonly screenshots?: readonly {
readonly light?: string | undefined;
readonly dark?: string | undefined;
}[] | undefined;
readonly icon?: {
readonly key: string;
readonly hue?: string | undefined;
} | undefined;
readonly dependsOn?: readonly string[] | undefined;
readonly spec?: string | undefined;
readonly createdAt?: string | undefined;
readonly author?: string | undefined;
}
profile
.
key: string
key
);
});

Each piece of state has a getter for a one-off read and an atom for a component that should update. getFailed lists plugins that failed to load or activate, with the phase and the error.

export const
const report: () => {
enabled: readonly string[];
activeModules: readonly string[];
failed: {
id: string;
phase: PluginManager.PluginFailurePhase;
reason: PluginManager.PluginFailureReason;
}[];
}
report
= () => ({
enabled: readonly string[]
enabled
:
const manager: PluginManager.PluginManager
manager
.
PluginManager.getEnabled(): readonly string[]
getEnabled
(),
activeModules: readonly string[]
activeModules
:
const manager: PluginManager.PluginManager
manager
.
PluginManager.getActive(): readonly string[]
getActive
(),
failed: {
id: string;
phase: PluginManager.PluginFailurePhase;
reason: PluginManager.PluginFailureReason;
}[]
failed
:
const manager: PluginManager.PluginManager
manager
.
PluginManager.getFailed(): readonly PluginManager.PluginFailure[]
getFailed
()
.
ReadonlyArray<PluginFailure>.map<{
id: string;
phase: PluginManager.PluginFailurePhase;
reason: PluginManager.PluginFailureReason;
}>(callbackfn: (value: PluginManager.PluginFailure, index: number, array: readonly PluginManager.PluginFailure[]) => {
id: string;
phase: PluginManager.PluginFailurePhase;
reason: PluginManager.PluginFailureReason;
}, thisArg?: any): {
id: string;
phase: PluginManager.PluginFailurePhase;
reason: PluginManager.PluginFailureReason;
}[]

Calls a defined callback function on each element of an array, and returns an array that contains the results.

@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.

@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.

map
(({
id: string
id
,
phase: PluginManager.PluginFailurePhase
phase
,
reason: PluginManager.PluginFailureReason
reason
}) => ({
id: string
id
,
phase: PluginManager.PluginFailurePhase
phase
,
reason: PluginManager.PluginFailureReason
reason
})),
});
// The same state as atoms, for a component that should update.
export const
const enabledAtom: Atom<readonly string[]>
enabledAtom
=
const manager: PluginManager.PluginManager
manager
.
PluginManager.enabled: Atom<readonly string[]>
enabled
;
Member What it does
make(options) Creates a manager.
start(), activate(event) Runs startup; fires an event.
add(id), enable(id), disable(id), remove(id) Changes which plugins are loaded and on.
plugins, enabled, active, failed Atoms of the manager’s state; each has a get… reader.
getDependencies(id), getDependents(id) The plugins a plugin needs, or that need it.
capabilities The capability manager that holds every contribution.
Page What it covers
Activation When the manager activates each module.
Plugin Defining the plugins the manager holds.
Testing a plugin A manager in a headless test app.