A plugin’s metadata names it: a key that no other plugin uses, a display name, a description and an icon. Its
modules do the work. Each module activates on an event, runs once, and contributes
capabilities: typed values, such as a surface component or an operation handler,
that other modules look up.
Plugin.define takes the metadata, Plugin.addModule adds each module, and Plugin.make returns a function that the
app’s plugin manager calls to create the plugin.
Adds a module to a plugin builder. Supports both pipeline and direct call styles.
Accepts either:
A spec-carrying module (
Capability.lazyModule
/
Capability.inlineModule
):
requires/provides/activatesOn/props all come from the module's own spec (declared where
it is authored), so the call needs only an optional id override.
An inline authoring record (or a function receiving plugin options): declares
provides/requires (dependency mode) or activatesOn + requires/provides
(runtime-event mode); the activate effect's environment is constrained to the declared
requires and its return must cover the declared provides.
Creates a Plugin from a builder.
Supports both pipeline and direct call styles.
Always returns a factory function (options: T) => Plugin.
When T is void, the function takes no arguments: () => Plugin.
make,
6
);
7
A plugin you publish describes itself in a dx.config.tsmanifest instead. Plugin.config takes the same
metadata, plus registry fields such as tags, and the list of modules. It returns the plugin’s meta and its make
function, so the registry can read the plugin without loading its code.
Author or organization name. Only used for bundled plugins. For plugins published to the
registry this field is ignored — the verified publisher (handle ?? did) is used instead.
author: 'Example',
17
description?: string | undefined
description:
18
'A reading list: save links with a note, and mark them as read.',
19
icon?: {
readonly key: string;
readonly hue?: string | undefined;
} | undefined
icon: {
key: string
key: 'ph--tray-arrow-down--regular',
hue?: string | undefined
hue: 'indigo' },
20
tags?: readonly string[] | undefined
tags: ['labs'],
21
},
22
modules: readonly (Module<any> | undefined)[]
Module specs only; each body is a dynamic import, so reading the config loads no plugin code.
Module contributing schemas. Prefer the loader form — schema objects ride whatever barrels
declare them, so an inline list drags those barrels into the plugin definition's static
closure (the boot evaluation floor); a loader keeps them in the module body chunk.
Module maker contributing operation handlers. Handler sets register eagerly by default: a
keyed set is a definition→loader map with a thin closure (definitions carry no
implementations), so the operation registry is complete at boot while each handler's BODY
still loads per invocation. The deferral axis is the operation, not the module.
A feature plugin whose operations cannot be invoked before the app is interactive declares
activatesOn: ActivationEvents.Idle to register in that wave instead of on the startup pass;
the boot path (deck, layout, space, client) invokes operations during startup and stays eager.
Stated explicitly rather than inherited: omitting activatesOn now normalizes to Idle, which
would leave the registry incomplete for exactly those boot-path invocations.
Module maker contributing a typed create-object entry. Gated by default on the create flow
opening (all consumers read the entries reactively); declare activatesOn to override.
Module maker contributing app-graph node builders. Gated by default on the host's idle event
rather than the contributing plugin's own start: a builder is what puts a plugin's items in
the navtree, so gating it on the plugin's own start (fired when its surface renders) is a
deadlock — the item never appears, so it can never be opened. Declare activatesOn to override.
Module maker contributing React surfaces. Declaring roles (the role tokens the module's
surfaces bind, e.g. AppSurface.Article or a plugin's <Plugin>Surface tokens) gates the module
on those roles' demand events
(
ActivationEvents.SurfacesRequested
) — it loads when a Surface for one of its roles
first renders instead of at startup. Modules without declared roles stay eager; an explicit
activatesOn wins over the derived gate.
Module maker contributing skill definitions. Gated by default on the assistant plugin's start
event — a skill belongs to the assistant feature regardless of which plugin contributes it
(skills register into a shared registry whose consumers are reactive); declare activatesOn
to override.
A module is inline or lazy. Capability.inlineModule carries its body with it. Capability.lazyModule
names a loader, and the plugin manager imports the body only when the module’s event fires. A module whose event
never fires never loads, so a plugin costs nothing until it is used.
Helper to define a lazily loaded module body with an eager requires/provides spec.
The spec is available without importing the chunk (dependency ordering happens before
code-splitting resolves) and the loaded default export is type-checked against it. The
returned
Module
is opaque with respect to requires/provides/props, parameterized
only by Options — this is what eliminates TS2883 at every call site.
The lazy pairing of
makeModule
.
@param ― name The export name (e.g., 'AppGraphBuilder') - used to auto-compute module IDs.
@param ― spec The requires/provides declaration, matching the module authoring site.
Activates when this runtime event fires instead of during the dependency pass.
activatesOn:
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
Helper to define a capability module with explicit typing.
Wraps the default export function to provide better type inference and make the pattern explicit.
This helper provides explicit typing for the module activation function,
making it clear that the function should:
Access declared requires via yield* (or the ambient Capability.Service/Plugin.Service)
The module makers in AppCapability, such as AppCapability.surface, fix a module’s capability and its event, so a
manifest names only the file to load. The loader’s default export is checked against the capability at compile time.
Every module declares what it provides and what it requires. The plugin manager uses them to order activation;
Activation covers the rules.
Plugins never import each other’s modules. A plugin that extends another contributes to a capability the other
reads.
A plugin’s key is a reverse-domain name, and its last segment must be camelCase. Keep the key once the plugin has
users: settings and enabled state are stored under it.
A module keeps no state between activations. What it needs to keep, it contributes or stores.